From e806928db6eb359442496b4e3f0776fefd64e12c Mon Sep 17 00:00:00 2001 From: Patrick Simpson Date: Thu, 1 Jun 2017 15:03:51 +0200 Subject: [PATCH] [KOE-117] Rewrote search query encoding in an attempt to prevent reminders from popping up. Also limiting rewriting of query to prevent writing back if unchanged. Added the "RemindersKeepRunning" option to make it possible to stop the query when updating. --- .../AcaciaZPushPlugin.csproj | 4 +- .../SharedFolders/FeatureSharedFolders.cs | 9 + .../Features/SharedFolders/RemindersQuery.cs | 49 +++- .../AcaciaZPushPlugin/Native/Kernel32.cs | 29 ++ .../AcaciaZPushPlugin/Native/MAPI/Binary.cs | 49 ++-- .../AcaciaZPushPlugin/Native/MAPI/MAPI.cs | 21 ++ .../AcaciaZPushPlugin/Native/MAPI/Property.cs | 38 +-- .../Native/MAPI/Restriction.cs | 251 ++++++------------ .../Native/MAPI/RestrictionEncoder.cs | 249 +++++++++++++++++ .../AcaciaZPushPlugin/Native/NativeEncoder.cs | 175 ------------ .../AcaciaZPushPlugin/Stubs/IFolder.cs | 6 + .../Stubs/OutlookWrappers/FolderWrapper.cs | 72 ++++- .../AcaciaZPushPlugin/Utils/ImageUtils.cs | 10 +- .../AcaciaZPushPlugin/Utils/StringUtil.cs | 7 + .../AcaciaZPushPlugin/Utils/Util.cs | 6 + 15 files changed, 552 insertions(+), 423 deletions(-) create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/Kernel32.cs create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/MAPI.cs create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/RestrictionEncoder.cs delete mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/NativeEncoder.cs diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index ac56f78..ea3dcfe 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -310,13 +310,15 @@ + + - + diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs index 783ec46..a0d6c11 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs @@ -46,6 +46,15 @@ namespace Acacia.Features.SharedFolders } private static readonly BoolOption OPTION_REMINDERS = new BoolOption("Reminders", true); + [AcaciaOption("If disabled, the reminders queyr will be explicitly stopped and started when update the query. " + + "This causes more effort to search again, but might prevent issues.")] + public bool RemindersKeepRunning + { + get { return GetOption(OPTION_REMINDERS_KEEP_RUNNING); } + set { SetOption(OPTION_REMINDERS_KEEP_RUNNING, value); } + } + private static readonly BoolOption OPTION_REMINDERS_KEEP_RUNNING = new BoolOption("RemindersKeepRunning", true); + #endregion public override void Startup() diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/RemindersQuery.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/RemindersQuery.cs index 52504bb..dce9cf5 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/RemindersQuery.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/RemindersQuery.cs @@ -14,15 +14,16 @@ namespace Acacia.Features.SharedFolders { private static readonly SearchQuery.PropertyIdentifier PROP_FOLDER = new SearchQuery.PropertyIdentifier(PropTag.FromInt(0x6B20001F)); - private readonly LogContext _context; + private readonly FeatureSharedFolders _feature; private readonly IFolder _folder; private SearchQuery _queryRoot; private SearchQuery.Or _queryCustom; + private bool _queryCustomModified; - public RemindersQuery(LogContext context, IStore store) + public RemindersQuery(FeatureSharedFolders feature, IStore store) { - this._context = context; - _folder = store.GetSpecialFolder(SpecialFolder.Reminders); + this._feature = feature; + this._folder = store.GetSpecialFolder(SpecialFolder.Reminders); } public bool Open() @@ -31,10 +32,10 @@ namespace Acacia.Features.SharedFolders return true; try { - _queryRoot = _folder.SearchCriteria; + _queryRoot = FolderQuery; if (!(_queryRoot is SearchQuery.And)) return false; - Logger.Instance.Trace(this, "Current query1: {0}", _queryRoot.ToString()); + Logger.Instance.Debug(this, "Current query:\n{0}", _queryRoot.ToString()); SearchQuery.And root = (SearchQuery.And)_queryRoot; // TODO: more strict checking of query @@ -50,7 +51,6 @@ namespace Acacia.Features.SharedFolders // We have the root, but not the custom query. Create it. Logger.Instance.Debug(this, "Creating custom query"); - Logger.Instance.Trace(this, "Current query: {0}", root.ToString()); _queryCustom = new SearchQuery.Or(); // Add the prefix exclusion for shared folders @@ -63,11 +63,10 @@ namespace Acacia.Features.SharedFolders ); root.Operands.Add(_queryCustom); - Logger.Instance.Trace(this, "Modified query: {0}", root.ToString()); + Logger.Instance.Debug(this, "Modified query:\n{0}", root.ToString()); // Store it - // TODO: could store it on change only - _folder.SearchCriteria = root; - Logger.Instance.Trace(this, "Modified query2: {0}", _folder.SearchCriteria.ToString()); + FolderQuery = root; + Logger.Instance.Debug(this, "Modified query readback:\n{0}", FolderQuery); } catch (Exception e) { @@ -80,7 +79,7 @@ namespace Acacia.Features.SharedFolders { get { - return _context.LogContextId; + return _feature.LogContextId; } } @@ -91,7 +90,29 @@ namespace Acacia.Features.SharedFolders public void Commit() { - _folder.SearchCriteria = _queryRoot; + if (_queryCustomModified) + { + FolderQuery = _queryRoot; + _queryCustomModified = false; + } + } + + private SearchQuery FolderQuery + { + get + { + return _folder.SearchCriteria; + } + set + { + if (!_feature.RemindersKeepRunning) + _folder.SearchRunning = false; + + _folder.SearchCriteria = value; + + if (!_feature.RemindersKeepRunning) + _folder.SearchRunning = true; + } } public void UpdateReminders(SyncId folderId, bool wantReminders) @@ -125,6 +146,7 @@ namespace Acacia.Features.SharedFolders _queryCustom.Operands.Add(new SearchQuery.PropertyContent( PROP_FOLDER, SearchQuery.ContentMatchOperation.Prefix, SearchQuery.ContentMatchModifiers.None, prefix )); + _queryCustomModified = true; } } @@ -154,6 +176,7 @@ namespace Acacia.Features.SharedFolders Logger.Instance.Trace(this, "Unwanted prefix at {0}: {1}", i, prefix); _queryCustom.Operands.RemoveAt(i); + _queryCustomModified = true; } else ++i; } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/Kernel32.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/Kernel32.cs new file mode 100644 index 0000000..a34f22d --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/Kernel32.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Native +{ + public static class Kernel32 + { + [DllImport("kernel32.dll", SetLastError = false)] + public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + // ZeroMemory is actually a macro + public static void ZeroMemory(IntPtr ptr, int length) + { + for (int i = 0; i < length / 8; i += 8) + { + Marshal.WriteInt64(ptr, i, 0x00); + } + + for (int i = length % 8; i < -1; i--) + { + Marshal.WriteByte(ptr, length - i, 0x00); + } + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Binary.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Binary.cs index ee8702b..ab7421f 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Binary.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Binary.cs @@ -24,35 +24,40 @@ using System.Threading.Tasks; namespace Acacia.Native.MAPI { + /// + /// Simple wrapper type to allow string encoding to work. Returned from SBinary.Unmarshall; this is needed + /// as a copy of the data ptr there must be managed. + /// + public struct SBinaryWrapper + { + public readonly byte[] Data; + + public SBinaryWrapper(byte[] bytes) + { + this.Data = bytes; + } + + public override string ToString() + { + return "cb: " + Data.Length.ToString() + " lpb: " + StringUtil.BytesToHex(Data); + } + } unsafe public struct SBinary { public uint cb; public byte* ptr; - public byte[] Unmarshal() + public SBinaryWrapper Unmarshal() { byte[] result = new byte[cb]; - System.Runtime.InteropServices.Marshal.Copy((IntPtr)ptr, result, 0, result.Length); - return result; - } - - /// - /// Returns an instance with the data allocated in the enocder. - /// - public SBinary Marshal(NativeEncoder encoder) - { - return new SBinary() - { - cb = cb, - ptr = (byte*)encoder.Allocate(Unmarshal()) - }; + Marshal.Copy((IntPtr)ptr, result, 0, result.Length); + return new SBinaryWrapper(result); } public override string ToString() { - byte[] b = Unmarshal(); - return b.Length.ToString() + ":" + StringUtil.BytesToHex(b); + return Unmarshal().ToString(); } } @@ -60,15 +65,5 @@ namespace Acacia.Native.MAPI { public uint count; public SBinary* ptr; - - public byte[][] Unmarshal() - { - byte[][] result = new byte[count][]; - for (uint i = 0; i < count; ++i) - { - result[i] = ptr[i].Unmarshal(); - } - return result; - } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/MAPI.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/MAPI.cs new file mode 100644 index 0000000..74a6142 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/MAPI.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Native.MAPI +{ + public static class MAPI + { + [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)] + public static extern IntPtr MAPIAllocateBuffer(uint cbSize, ref IntPtr lppBuffer); + + [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)] + public static extern IntPtr MAPIAllocateMore(uint cbSize, IntPtr lpObject, ref IntPtr lppBuffer); + + [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)] + public static extern uint MAPIFreeBuffer(IntPtr lpBuffer); + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs index 7a20d48..2e9d401 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs @@ -20,7 +20,6 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using static Acacia.Native.NativeEncoder; namespace Acacia.Native.MAPI { @@ -48,7 +47,7 @@ namespace Acacia.Native.MAPI public override string ToString() { - return "<" + prop.ToString("X4") + ":" + type + ">"; + return string.Format("0x{0:X4}{1:X4} (PT_{2})", prop, (int)type, type); } public SearchQuery.PropertyIdentifier ToPropertyIdentifier() @@ -144,33 +143,42 @@ namespace Acacia.Native.MAPI case PropType.UNICODE: return new string(data.lpszW); case PropType.BINARY: - return data.bin; + return data.bin.Unmarshal(); } throw new NotImplementedException(); } - unsafe public static IntPtr MarshalFromObject(NativeEncoder encoder, PropTag prop, object value) + unsafe public static IntPtr MarshalFromObject(RestrictionEncoder encoder, PropTag prop, object value) { - PropValue obj = new PropValue(); - obj.header.ulPropTag = prop; - + IntPtr ptr; + Data data = new Data(); switch (prop.type) { case PropType.BOOLEAN: - obj.data.b = (bool)value; - return encoder.Allocate(obj.header, obj.data.b); + data.b = (bool)value; + ptr = encoder.AllocateWithExtra
(8, data.b); + break; case PropType.STRING8: - IntPtr ptrA = encoder.Allocate(Encoding.ASCII.GetBytes((string)value), new byte[] { 0 }); - return encoder.Allocate(obj.header, 8, ptrA); + IntPtr lpszA = encoder.Allocate(Encoding.ASCII.GetBytes((string)value), 1); + ptr = encoder.AllocateWithExtra
(8, lpszA); + break; case PropType.UNICODE: - IntPtr ptrW = encoder.Allocate(Encoding.Unicode.GetBytes((string)value), new byte[] { 0, 0 }); - return encoder.Allocate(obj.header, 8, ptrW); + IntPtr lpszW = encoder.Allocate(Encoding.Unicode.GetBytes((string)value), 2); + ptr = encoder.AllocateWithExtra
(8, lpszW); + break; case PropType.BINARY: - obj.data.bin = ((SBinary)value).Marshal(encoder); - return encoder.Allocate(obj.header, 8, obj.data.bin); + data.bin.cb = (uint)((SBinaryWrapper)value).Data.Length; + data.bin.ptr = (byte*)encoder.Allocate(((SBinaryWrapper)value).Data); + ptr = encoder.AllocateWithExtra
(8, data.bin); + break; default: throw new NotImplementedException(); } + + // Initialise the header + PropValue* obj = (PropValue*)ptr; + obj->header.ulPropTag = prop; + return ptr; } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Restriction.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Restriction.cs index 23c70c3..ea20923 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Restriction.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Restriction.cs @@ -39,20 +39,42 @@ namespace Acacia.Native.MAPI public PropTag ulPropTag; public PropValue* prop; + private static readonly string[] RELOP_NAMES = + { + "RELOP_LT", "RELOP_LE", "RELOP_GT", "RELOP_GE", "RELOP_EQ", "RELOP_NE", "RELOP_RE" + }; + public string ToString(int depth) { - string indent = new string(' ', depth); - string s = indent + relop + ":" + ulPropTag.ToString(); - s += ":" + prop->ToString(); - s += "\n"; + string s = string.Format( + "{0}lpRes->res.resProperty.relop = {2} = 0x{1:X8}\n" + + "{0}lpRes->res.resProperty.ulPropTag = {3}\n", + SRestriction.Indent(depth), + (int)relop, + RELOP_NAMES[(int)relop], + ulPropTag + ); + + s += string.Format("{0}lpRes->res.resProperty.lpProp->ulPropTag = {1}\n", + SRestriction.Indent(depth), + ulPropTag + ); + + s += string.Format("{0}lpRes->res.resProperty.lpProp->Value = {1}\n", + SRestriction.Indent(depth), + *prop + ); + return s; } public SearchQuery ToSearchQuery() { + object value = prop->ToObject(); return new SearchQuery.PropertyCompare(ulPropTag.ToPropertyIdentifier(), (SearchQuery.ComparisonOperation)(int)relop, - prop->ToObject()); + value + ); } } @@ -76,10 +98,24 @@ namespace Acacia.Native.MAPI public string ToString(int depth) { - string indent = new string(' ', depth); - string s = indent + ulFuzzyLevel + ":" + ulPropTag.ToString(); - s += ":" + prop->ToString(); - s += "\n"; + string s = string.Format( + "{0}lpRes->res.resContent.ulFuzzyLevel = FL_{2} = 0x{1:X8}\n" + + "{0}lpRes->res.resContent.ulPropTag = {3}\n", + SRestriction.Indent(depth), + (int)ulFuzzyLevel, + ulFuzzyLevel, + ulPropTag + ); + + s += string.Format("{0}lpRes->res.resContent.lpProp->ulPropTag = {1}\n", + SRestriction.Indent(depth), + ulPropTag + ); + + s += string.Format("{0}lpRes->res.resContent.lpProp->Value = {1}\n", + SRestriction.Indent(depth), + *prop + ); return s; } @@ -120,19 +156,20 @@ namespace Acacia.Native.MAPI public uint cb; public SRestriction* ptr; - public string ToString(int depth) + public string ToString(string name, int depth) { - string s = ""; + string s = string.Format("{0}lpRes->res.res{1}.cRes = 0x{2:X8}\n", SRestriction.Indent(depth), name, cb); for (uint i = 0; i < cb; ++i) { - s += ptr[i].ToString(depth); + s += string.Format("{0}lpRes->res.res{1}.lpRes[0x{2:X8}]\n", SRestriction.Indent(depth), name, i); + s += ptr[i].ToString(depth + 1); } return s; } public SearchQuery ToSearchQuery(bool and) { - SearchQuery.MultiOperator oper = and ? (SearchQuery.MultiOperator)new SearchQuery.And() : new SearchQuery.Or(); ; + SearchQuery.MultiOperator oper = and ? (SearchQuery.MultiOperator)new SearchQuery.And() : new SearchQuery.Or(); for (uint i = 0; i < cb; ++i) { oper.Add(ptr[i].ToSearchQuery()); @@ -148,7 +185,10 @@ namespace Acacia.Native.MAPI public string ToString(int depth) { - return ptr->ToString(depth); + string s = string.Format("{0}lpRes->res.resNot.ulReserved = 0x{1:X8}\n", SRestriction.Indent(depth), dwReserved); + s += string.Format("{0}lpRes->res.resNot.lpRes\n", SRestriction.Indent(depth)); + s += ptr->ToString(depth + 1); + return s; } public SearchQuery ToSearchQuery() @@ -175,8 +215,10 @@ namespace Acacia.Native.MAPI } public string ToString(int depth) { - string indent = new string(' ', depth); - return indent + ToString() + "\n"; + string s = string.Format("{0}lpRes->res.resBitMask.relBMR = BMR_{1} = 0x{2:X8}\n", SRestriction.Indent(depth), bmr, (int)bmr); + s += string.Format("{0}lpRes->res.resBitMask.ulMask = 0x{1:X8}\n", SRestriction.Indent(depth), mask); + s += string.Format("{0}lpRes->res.resBitMask.ulPropTag = 0x{1:X8}\n", SRestriction.Indent(depth), prop); + return s; } public SearchQuery ToSearchQuery() @@ -197,8 +239,10 @@ namespace Acacia.Native.MAPI } public string ToString(int depth) { - string indent = new string(' ', depth); - return indent + prop.ToString() + "\n"; + string s = string.Format("{0}lpRes->res.resExist.ulPropTag = 0x{1:X8}\n", SRestriction.Indent(depth), prop); + s += string.Format("{0}lpRes->res.resExist.ulReserved1 = 0x{1:X8}\n", SRestriction.Indent(depth), dwReserved1); + s += string.Format("{0}lpRes->res.resExist.ulReserved2 = 0x{1:X8}\n", SRestriction.Indent(depth), dwReserved2); + return s; } public SearchQuery ToSearchQuery() @@ -281,30 +325,34 @@ namespace Acacia.Native.MAPI return ToString(0); } + internal static string Indent(int depth) + { + return new string('\t', depth); + } + public string ToString(int depth) { - string indent = new string(' ', depth); - string s = indent + header.rt.ToString() + "\n" + indent + "{\n"; + string s = Indent(depth) + string.Format("lpRes->rt = 0x{0:X} = RES_{1}\n", (int)header.rt, header.rt); switch (header.rt) { case RestrictionType.AND: case RestrictionType.OR: - s += data.sub.ToString(depth + 1); + s += data.sub.ToString(header.rt.ToString().ToTitle(), depth); break; case RestrictionType.NOT: - s += data.not.ToString(depth + 1); + s += data.not.ToString(depth); break; case RestrictionType.CONTENT: - s += data.content.ToString(depth + 1); + s += data.content.ToString(depth); break; case RestrictionType.PROPERTY: - s += data.prop.ToString(depth + 1); + s += data.prop.ToString(depth); break; case RestrictionType.BITMASK: - s += data.bitMask.ToString(depth + 1); + s += data.bitMask.ToString(depth); break; case RestrictionType.EXIST: - s += data.exist.ToString(depth + 1); + s += data.exist.ToString(depth); break; /* TODO COMPAREPROPS, @@ -316,158 +364,7 @@ namespace Acacia.Native.MAPI ANNOTATION*/ } - s += indent + "}\n"; return s; } } - - /// - /// Encodes a search as an SRestriction. Note that as memory needs to be managed for the miscellaneous structures, - /// the SRestriction is only valid until RestrictionEncoder is disposed. - /// - unsafe public class RestrictionEncoder : NativeEncoder, ISearchEncoder - { - private class EncodingStack - { - public SRestriction[] array; - public int index; - public SRestriction* ptr; - - public EncodingStack(int count, Allocation alloc) - { - array = alloc.Object; - index = 0; - ptr = (SRestriction*)alloc.Pointer; - } - } - private readonly Stack _current = new Stack(); - private readonly EncodingStack _root; - - public RestrictionEncoder() - { - // Create an object for the root element - _root = Begin(1); - } - - protected override void DoRelease() - { - base.DoRelease(); - } - - public SRestriction Restriction - { - get { return _root.array[0]; } - } - - private SRestriction* Current - { - get - { - EncodingStack top = _current.Peek(); - return top.ptr + top.index; - } - } - - public void Encode(SearchQuery.PropertyExists part) - { - Current->header.rt = RestrictionType.EXIST; - Current->data.exist.prop = part.Property.Tag; - } - - public void Encode(SearchQuery.Or part) - { - Current->header.rt = RestrictionType.OR; - Current->data.sub.cb = (uint)part.Operands.Count; - Current->data.sub.ptr = EncodePointer(part.Operands); - } - - public void Encode(SearchQuery.PropertyIdentifier part) - { - // This should be unreachable - throw new InvalidProgramException(); - } - - public void Encode(SearchQuery.Not part) - { - Current->header.rt = RestrictionType.NOT; - Current->data.not.ptr = EncodePointer(new[] { part.Operand }); - } - - public void Encode(SearchQuery.And part) - { - Current->header.rt = RestrictionType.AND; - Current->data.sub.cb = (uint)part.Operands.Count; - Current->data.sub.ptr = EncodePointer(part.Operands); - } - - private SRestriction* EncodePointer(IEnumerable operands) - { - EncodingStack alloc = Begin(operands.Count()); - try - { - foreach (SearchQuery operand in operands) - { - operand.Encode(this); - ++alloc.index; - } - } - finally - { - End(); - } - return alloc.ptr; - } - - private EncodingStack Begin(int count) - { - // Allocate and push the array - EncodingStack alloc = new EncodingStack(count, Allocate(new SRestriction[count])); - _current.Push(alloc); - - return alloc; - } - - private void End() - { - _current.Pop(); - } - - public void Encode(SearchQuery.PropertyContent part) - { - Current->header.rt = RestrictionType.CONTENT; - Current->data.content.ulFuzzyLevel = ContentRestriction.FuzzyLevelFromSearchQuery(part); - Current->data.content.ulPropTag = part.Property.Tag; - Current->data.content.prop = (PropValue*)PropValue.MarshalFromObject(this, part.Property.Tag, part.Content); - } - - public void Encode(SearchQuery.PropertyCompare part) - { - Current->header.rt = RestrictionType.PROPERTY; - Current->data.prop.relop = (SearchOperation)part.Operation; - Current->data.prop.ulPropTag = part.Property.Tag; - Current->data.prop.prop = (PropValue*)PropValue.MarshalFromObject(this, part.Property.Tag, part.Value); - } - - public void Encode(SearchQuery.PropertyBitMask part) - { - Current->header.rt = RestrictionType.BITMASK; - Current->data.bitMask.bmr = (BMR)(int)part.Operation; - Current->data.bitMask.prop = part.Property.Tag; - Current->data.bitMask.mask = part.Mask; - } - } - - public static class RestrictionExensions - { - /// - /// Encodes the search as an SRestriction. - /// - /// The encoder containing the restriction. The caller is responsible for disposing. - public static RestrictionEncoder ToRestriction(this SearchQuery search) - { - RestrictionEncoder encoder = new RestrictionEncoder(); - search.Encode(encoder); - return encoder; - } - } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/RestrictionEncoder.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/RestrictionEncoder.cs new file mode 100644 index 0000000..ea0b952 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/RestrictionEncoder.cs @@ -0,0 +1,249 @@ +using Acacia.Stubs; +using Acacia.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Native.MAPI +{ + + /// + /// Encodes a search as an SRestriction. Note that as memory needs to be managed for the miscellaneous structures, + /// the SRestriction is only valid until RestrictionEncoder is disposed. + /// + unsafe public class RestrictionEncoder : DisposableWrapper, ISearchEncoder + { + private class Allocation + { + public readonly IntPtr ptr; + private readonly int count; + private int index; + + public SRestriction* Current + { + get + { + if (index >= count) + throw new InvalidProgramException(); + return Pointer + index; + } + } + public SRestriction* Pointer { get { return (SRestriction*)ptr; } } + + /// + /// Constructor. Allocates the memory for the object. + /// + /// The root allocation, or null if this is the root. All allocations will be added to the root. + /// The number of SRestriction objects to allocate + public Allocation(RestrictionEncoder encoder, int count) + { + this.count = count; + // Allocate the buffer + ptr = encoder.AllocateArray(count); + } + + public void Next() + { + ++index; + } + } + private Allocation _root; + private readonly Stack _stack = new Stack(); + + public RestrictionEncoder() + { + // Push the root entry + _root = Push(1); + } + + protected override void DoRelease() + { + if (_root != null) + { + uint res = MAPI.MAPIFreeBuffer(_root.ptr); + if (res != 0) + { + // TODO: log? + } + _root = null; + } + } + + public SRestriction* Encoded + { + get { return _root.Pointer; } + } + + public SRestriction* Current + { + get { return _stack.Peek().Current; } + } + + private Allocation Push(int count = 1) + { + Allocation alloc = new Allocation(this, count); + _stack.Push(alloc); + return alloc; + } + + private void Pop(Allocation expected) + { + Allocation alloc = _stack.Pop(); + if (expected != alloc) + throw new InvalidProgramException(); + } + + public void Encode(SearchQuery.PropertyExists part) + { + Current->header.rt = RestrictionType.EXIST; + Current->data.exist.prop = part.Property.Tag; + } + + public void Encode(SearchQuery.Or part) + { + Current->header.rt = RestrictionType.OR; + Current->data.sub.cb = (uint)part.Operands.Count; + Current->data.sub.ptr = EncodePointer(part.Operands); + } + + public void Encode(SearchQuery.And part) + { + Current->header.rt = RestrictionType.AND; + Current->data.sub.cb = (uint)part.Operands.Count; + Current->data.sub.ptr = EncodePointer(part.Operands); + } + + public void Encode(SearchQuery.PropertyIdentifier part) + { + // This should be unreachable + throw new InvalidProgramException(); + } + + public void Encode(SearchQuery.Not part) + { + Current->header.rt = RestrictionType.NOT; + Current->data.not.ptr = EncodePointer(new[] { part.Operand }); + } + + private SRestriction* EncodePointer(IEnumerable operands) + { + Allocation alloc = Push(operands.Count()); + try + { + foreach (SearchQuery operand in operands) + { + operand.Encode(this); + alloc.Next(); + } + } + finally + { + Pop(alloc); + } + return alloc.Pointer; + } + + public void Encode(SearchQuery.PropertyContent part) + { + Current->header.rt = RestrictionType.CONTENT; + Current->data.content.ulFuzzyLevel = ContentRestriction.FuzzyLevelFromSearchQuery(part); + Current->data.content.ulPropTag = part.Property.Tag; + Current->data.content.prop = (PropValue*)PropValue.MarshalFromObject(this, part.Property.Tag, part.Content); + } + + public void Encode(SearchQuery.PropertyCompare part) + { + Current->header.rt = RestrictionType.PROPERTY; + Current->data.prop.relop = (SearchOperation)part.Operation; + Current->data.prop.ulPropTag = part.Property.Tag; + Current->data.prop.prop = (PropValue*)PropValue.MarshalFromObject(this, part.Property.Tag, part.Value); + } + + public void Encode(SearchQuery.PropertyBitMask part) + { + Current->header.rt = RestrictionType.BITMASK; + Current->data.bitMask.bmr = (BMR)(int)part.Operation; + Current->data.bitMask.prop = part.Property.Tag; + Current->data.bitMask.mask = part.Mask; + } + + public IntPtr AllocateArray(int count) + { + // Try to just determine the size based on the type. If that fails, determine the size of a default object + int structSize = Marshal.SizeOf(typeof(StructType)); + return AllocateRaw(structSize * count); + } + + public IntPtr AllocateWithExtra(int alignExtra, object extra) + { + // Determine the size + int size = Marshal.SizeOf(); + size = Util.Align(size, alignExtra); + + int extraOffset = size; + size += Marshal.SizeOf(extra); + + // Allocate + IntPtr ptr = AllocateRaw(size); + + // Copy the extra structure + Marshal.StructureToPtr(extra, ptr + extraOffset, false); + return ptr; + } + + private IntPtr AllocateRaw(int size) + { + IntPtr res; + IntPtr ptr = IntPtr.Zero; + if (_root == null) + { + res = MAPI.MAPIAllocateBuffer((uint)size, ref ptr); + } + else + { + res = MAPI.MAPIAllocateMore((uint)size, _root.ptr, ref ptr); + } + + if (res != IntPtr.Zero) + throw new InvalidOperationException("MAPI Allocation failed: " + res); + + // Zero it out to prevent issues + Kernel32.ZeroMemory(ptr, size); + + return ptr; + } + + /// + /// Allocates a copy of the byte array. + /// + /// The byte array + /// Number of additional zero bytes, for padding + public IntPtr Allocate(byte[] bytes, int zeros = 0) + { + IntPtr ptr = AllocateRaw(bytes.Length + zeros); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + for (int i = 0; i < zeros; ++i) + { + ((byte*)ptr)[bytes.Length + i] = 0; + } + return ptr; + } + } + + public static class RestrictionExensions + { + /// + /// Encodes the search as an SRestriction. + /// + /// The encoder containing the restriction. The caller is responsible for disposing. + public static RestrictionEncoder ToRestriction(this SearchQuery search) + { + RestrictionEncoder encoder = new RestrictionEncoder(); + search.Encode(encoder); + return encoder; + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/NativeEncoder.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/NativeEncoder.cs deleted file mode 100644 index 1fe0ce5..0000000 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/NativeEncoder.cs +++ /dev/null @@ -1,175 +0,0 @@ -using Acacia.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Acacia.Native.MAPI; - -namespace Acacia.Native -{ - /// - /// Helper for encoding objects into native structures. - /// - abstract public class NativeEncoder : DisposableWrapper - { - protected class AllocationBase : IDisposable - { - protected readonly object _obj; - protected readonly GCHandle _handle; - protected readonly IntPtr _ptr; - - public AllocationBase(Type type, int size) - { - _ptr = Marshal.AllocHGlobal(size); - } - - public AllocationBase(object obj) - { - this._obj = obj; - _handle = GCHandle.Alloc(obj, GCHandleType.Pinned); - _ptr = _handle.AddrOfPinnedObject(); - } - - public IntPtr Pointer { get { return _ptr; } } - - public void Dispose() - { - if (_handle.IsAllocated) - _handle.Free(); - else - Marshal.FreeHGlobal(_ptr); - } - } - - unsafe protected class Allocation : AllocationBase - { - internal Allocation(int size) : base(typeof(ObjType), size) - { - } - - internal Allocation(ObjType obj) : base(obj) - { - } - - public ObjType Object - { - get - { - return (ObjType)_obj; - } - } - } - - private readonly List _allocs = new List(); - - override protected void DoRelease() - { - foreach(AllocationBase alloc in _allocs) - alloc.Dispose(); - } - - protected AllocationBase Allocate(int size) - { - AllocationBase alloc = new AllocationBase(typeof(object), size); - _allocs.Add(alloc); - return alloc; - } - - protected Allocation Allocate(ObjType obj) - { - Allocation alloc = new Allocation(obj); - _allocs.Add(alloc); - return alloc; - } - - protected Allocation Allocate() - { - return Allocate(Activator.CreateInstance()); - } - - // TODO: put in lib - [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] - private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); - - public IntPtr Allocate(ElementType[] obj, params ElementType[][] additional) - { - ElementType[][] all = new ElementType[][] { obj }.Concat(additional).ToArray(); - - int size = 0; - int[] starts = new int[all.Length]; - int[] sizes = new int[all.Length]; - for (int i = 0; i < all.Length; ++i) - { - starts[i] = size; - int thisSize = ((Array)all[i]).Length * Marshal.SizeOf(); - sizes[i] = thisSize; - size += thisSize; - } - - AllocationBase alloc = Allocate(size); - IntPtr ptr = alloc.Pointer; - for (int i = 0; i < all.Length; ++i) - { - GCHandle handle = GCHandle.Alloc(all[i], GCHandleType.Pinned); - try - { - CopyMemory(ptr + starts[i], handle.AddrOfPinnedObject(), (uint)sizes[i]); - } - finally - { - handle.Free(); - } - } - return alloc.Pointer; - } - - /// - /// Returns a block of memory containing all specified objects sequentially. - /// - public IntPtr Allocate(object obj, params object[] additional) - { - object[] all = new object[] { obj }.Concat(additional).ToArray(); - - int size = 0; - int[] starts = new int[all.Length]; - object[] encode = new object[all.Length]; - int used = 0; - int align = -1; - for (int i = 0; i < all.Length; ++i) - { - if (all[i] is int) - { - align = (int)all[i]; - ++i; - } - - starts[used] = Align(size, align); - int thisSize = Marshal.SizeOf(all[i]); - size = starts[used] + thisSize; - encode[used] = all[i]; - align = -1; - ++used; - } - - AllocationBase alloc = Allocate(size); - IntPtr ptr = alloc.Pointer; - for (int i = 0; i < used; ++i) - { - Marshal.StructureToPtr(encode[i], ptr + starts[i], false); - } - return alloc.Pointer; - } - - - - private int Align(int size, int align) - { - if (align < 0) - align = Marshal.SizeOf(); - int additional = (align - (size % align)) % align; - return size + additional; - } - } -} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IFolder.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IFolder.cs index 911f969..e79b7ff 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IFolder.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IFolder.cs @@ -121,6 +121,12 @@ namespace Acacia.Stubs set; } + bool SearchRunning + { + get; + set; + } + void Save(); void SetCustomIcon(IPicture icon); diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/FolderWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/FolderWrapper.cs index af92cbd..f7b3e6a 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/FolderWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/FolderWrapper.cs @@ -392,7 +392,56 @@ namespace Acacia.Stubs.OutlookWrappers set; } + #region Search criteria + unsafe public SearchQuery SearchCriteria + { + get + { + IMAPIFolder imapi = _item.MAPIOBJECT as IMAPIFolder; + SBinaryArray* sb1 = null; + SRestriction* restrict = null; + try + { + SearchCriteriaState state; + imapi.GetSearchCriteria(0, &restrict, &sb1, out state); + Logger.Instance.Debug(this, "GetSearchCriteria: {0}: {1}\n{2}", Name, state, + restrict == null ? "" : restrict->ToString()); + return restrict->ToSearchQuery(); + } + finally + { + MAPI.MAPIFreeBuffer((IntPtr)restrict); + MAPI.MAPIFreeBuffer((IntPtr)sb1); + ComRelease.Release(imapi); + } + } + + set + { + IMAPIFolder imapi = _item.MAPIOBJECT as IMAPIFolder; + try + { + using (RestrictionEncoder res = value.ToRestriction()) + { + SRestriction* resEncoded = res.Encoded; + Logger.Instance.Debug(this, "SetSearchCriteria: {0}\n{1}", Name, resEncoded == null ? "" : resEncoded->ToString()); + imapi.SetSearchCriteria(resEncoded, null, SearchCriteriaFlags.NONE); + } + } + catch (Exception e) + { + Logger.Instance.Error(this, "Exception in SetSearchCriteria: {0}: {1}", Name, e); + } + finally + { + ComRelease.Release(imapi); + } + } + + } + + unsafe public bool SearchRunning { get { @@ -400,10 +449,13 @@ namespace Acacia.Stubs.OutlookWrappers try { SearchCriteriaState state; - SBinaryArray* sb1; - SRestriction* restrict; - imapi.GetSearchCriteria(0, &restrict, &sb1, out state); - return restrict->ToSearchQuery(); + imapi.GetSearchCriteria(0, null, null, out state); + return (state & SearchCriteriaState.SEARCH_RUNNING) != 0; + } + catch (Exception e) + { + Logger.Instance.Error(this, "Exception in GetSearchRunning: {0}: {1}", Name, e); + return true; } finally { @@ -416,11 +468,11 @@ namespace Acacia.Stubs.OutlookWrappers IMAPIFolder imapi = _item.MAPIOBJECT as IMAPIFolder; try { - using (RestrictionEncoder res = value.ToRestriction()) - { - SRestriction restrict = res.Restriction; - imapi.SetSearchCriteria(&restrict, null, SearchCriteriaFlags.NONE); - } + imapi.SetSearchCriteria(null, null, value ? SearchCriteriaFlags.RESTART_SEARCH : SearchCriteriaFlags.STOP_SEARCH); + } + catch (Exception e) + { + Logger.Instance.Error(this, "Exception in SetSearchRunning: {0}: {1}", Name, e); } finally { @@ -428,5 +480,7 @@ namespace Acacia.Stubs.OutlookWrappers } } } + + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ImageUtils.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ImageUtils.cs index 80fdfd5..0b595b0 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ImageUtils.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ImageUtils.cs @@ -1,4 +1,6 @@ -/// Copyright 2017 Kopano b.v. + +using Acacia.Native; +/// 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,7 +15,6 @@ /// along with this program.If not, see. /// /// Consult LICENSE file for details - using System; using System.Collections.Generic; using System.Drawing; @@ -27,9 +28,6 @@ namespace Acacia.Utils { public static class ImageUtils { - [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] - private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); - public static Bitmap GetBitmapFromHBitmap(IntPtr nativeHBitmap) { @@ -49,7 +47,7 @@ namespace Acacia.Utils { IntPtr target = bmpData2.Scan0 + bmpData2.Stride * y; IntPtr source = bmpData.Scan0 + bmpData.Stride * y; - CopyMemory(target, source, (uint)Math.Abs(bmpData2.Stride)); + Kernel32.CopyMemory(target, source, (uint)Math.Abs(bmpData2.Stride)); } } finally diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs index aadf931..be87a90 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs @@ -49,6 +49,13 @@ namespace Acacia.Utils return _this; } + public static string ToTitle(this string _this) + { + if (string.IsNullOrWhiteSpace(_this)) + return _this; + return _this.Substring(0, 1).ToUpper() + _this.Substring(1).ToLower(); + } + #endregion diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs index 84b17c1..27bb50c 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs @@ -173,5 +173,11 @@ namespace Acacia.Utils } #endregion + + public static int Align(int size, int align) + { + int additional = (align - (size % align)) % align; + return size + additional; + } } }