mirror of
https://github.com/Kopano-dev/kopano-ol-extension.git
synced 2023-10-10 13:37:40 +02:00
[KOE-12] Finished encoding of search criteria for COM. Added code to modify search query to exclude shared folders, still need to add code to allow for selected folders.
This commit is contained in:
parent
e0356b0321
commit
8f5c08e4a9
@ -274,6 +274,7 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Features\SendAs\FeatureSendAs.cs" />
|
<Compile Include="Features\SendAs\FeatureSendAs.cs" />
|
||||||
<Compile Include="Features\SharedFolders\FolderTreeNode.cs" />
|
<Compile Include="Features\SharedFolders\FolderTreeNode.cs" />
|
||||||
|
<Compile Include="Features\SharedFolders\SharedCalendarReminders.cs" />
|
||||||
<Compile Include="Features\Signatures\FeatureSignatures.cs" />
|
<Compile Include="Features\Signatures\FeatureSignatures.cs" />
|
||||||
<Compile Include="Features\Signatures\SignaturesSettings.cs">
|
<Compile Include="Features\Signatures\SignaturesSettings.cs">
|
||||||
<SubType>UserControl</SubType>
|
<SubType>UserControl</SubType>
|
||||||
@ -291,6 +292,7 @@
|
|||||||
<Compile Include="Native\MAPI\IMAPIProp.cs" />
|
<Compile Include="Native\MAPI\IMAPIProp.cs" />
|
||||||
<Compile Include="Native\MAPI\Property.cs" />
|
<Compile Include="Native\MAPI\Property.cs" />
|
||||||
<Compile Include="Native\MAPI\Restriction.cs" />
|
<Compile Include="Native\MAPI\Restriction.cs" />
|
||||||
|
<Compile Include="Native\NativeEncoder.cs" />
|
||||||
<Compile Include="OutlookConstants.cs" />
|
<Compile Include="OutlookConstants.cs" />
|
||||||
<Compile Include="SearchQuery.cs" />
|
<Compile Include="SearchQuery.cs" />
|
||||||
<Compile Include="Stubs\Enums.cs" />
|
<Compile Include="Stubs\Enums.cs" />
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
using Acacia.Native.MAPI;
|
||||||
|
using Acacia.Stubs;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Acacia.Features.SharedFolders
|
||||||
|
{
|
||||||
|
public class SharedCalendarReminders : LogContext
|
||||||
|
{
|
||||||
|
private static readonly SearchQuery.PropertyIdentifier PROP_FOLDER = new SearchQuery.PropertyIdentifier(PropTag.FromInt(0x6B20001F));
|
||||||
|
|
||||||
|
private readonly LogContext _context;
|
||||||
|
public string LogContextId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _context.LogContextId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedCalendarReminders(LogContext context)
|
||||||
|
{
|
||||||
|
this._context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialise(IStore store)
|
||||||
|
{
|
||||||
|
using (IFolder reminders = store.GetSpecialFolder(SpecialFolder.Reminders))
|
||||||
|
{
|
||||||
|
SearchQuery.Or custom = FindCustomQuery(reminders, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchQuery.Or FindCustomQuery(IFolder reminders, bool addIfNeeded)
|
||||||
|
{
|
||||||
|
SearchQuery query = reminders.SearchCriteria;
|
||||||
|
if (!(query is SearchQuery.And))
|
||||||
|
return null;
|
||||||
|
Logger.Instance.Trace(this, "Current query1: {0}", query.ToString());
|
||||||
|
|
||||||
|
SearchQuery.And root = (SearchQuery.And)query;
|
||||||
|
// TODO: more strict checking of query
|
||||||
|
if (root.Operands.Count == 3)
|
||||||
|
{
|
||||||
|
SearchQuery.Or custom = root.Operands.ElementAt(2) as SearchQuery.Or;
|
||||||
|
if (custom != null)
|
||||||
|
{
|
||||||
|
// TODO: check property test
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have the root, but not the custom query. Create it if needed.
|
||||||
|
if (addIfNeeded)
|
||||||
|
{
|
||||||
|
Logger.Instance.Debug(this, "Creating custom query");
|
||||||
|
Logger.Instance.Trace(this, "Current query: {0}", root.ToString());
|
||||||
|
SearchQuery.Or custom = new SearchQuery.Or();
|
||||||
|
|
||||||
|
// Add the prefix exclusion for shared folders
|
||||||
|
custom.Add(
|
||||||
|
new SearchQuery.Not(
|
||||||
|
new SearchQuery.PropertyContent(
|
||||||
|
PROP_FOLDER, SearchQuery.ContentMatchOperation.Prefix, SearchQuery.ContentMatchModifiers.None, "S"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
root.Operands.Add(custom);
|
||||||
|
Logger.Instance.Trace(this, "Modified query: {0}", root.ToString());
|
||||||
|
reminders.SearchCriteria = root;
|
||||||
|
Logger.Instance.Trace(this, "Modified query2: {0}", reminders.SearchCriteria.ToString());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -122,7 +122,7 @@ namespace Acacia.Features.Signatures
|
|||||||
/// <param name="serverSignatureHash">The signature hash. If null, the hash will not be checked and a hard sync will be done.</param>
|
/// <param name="serverSignatureHash">The signature hash. If null, the hash will not be checked and a hard sync will be done.</param>
|
||||||
private void SyncSignatures(ZPushAccount account, string serverSignatureHash)
|
private void SyncSignatures(ZPushAccount account, string serverSignatureHash)
|
||||||
{
|
{
|
||||||
if (account == null || !account.Capabilities.Has("signatures"))
|
if (account?.Capabilities == null || !account.Capabilities.Has("signatures"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Check hash if needed
|
// Check hash if needed
|
||||||
|
@ -33,10 +33,22 @@ namespace Acacia.Native.MAPI
|
|||||||
public byte[] Unmarshal()
|
public byte[] Unmarshal()
|
||||||
{
|
{
|
||||||
byte[] result = new byte[cb];
|
byte[] result = new byte[cb];
|
||||||
Marshal.Copy((IntPtr)ptr, result, 0, result.Length);
|
System.Runtime.InteropServices.Marshal.Copy((IntPtr)ptr, result, 0, result.Length);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an instance with the data allocated in the enocder.
|
||||||
|
/// </summary>
|
||||||
|
public SBinary Marshal(NativeEncoder encoder)
|
||||||
|
{
|
||||||
|
return new SBinary()
|
||||||
|
{
|
||||||
|
cb = cb,
|
||||||
|
ptr = (byte*)encoder.Allocate(Unmarshal())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
byte[] b = Unmarshal();
|
byte[] b = Unmarshal();
|
||||||
|
@ -20,6 +20,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static Acacia.Native.NativeEncoder;
|
||||||
|
|
||||||
namespace Acacia.Native.MAPI
|
namespace Acacia.Native.MAPI
|
||||||
{
|
{
|
||||||
@ -52,7 +53,7 @@ namespace Acacia.Native.MAPI
|
|||||||
|
|
||||||
public SearchQuery.PropertyIdentifier ToPropertyIdentifier()
|
public SearchQuery.PropertyIdentifier ToPropertyIdentifier()
|
||||||
{
|
{
|
||||||
return SearchQuery.PropertyIdentifier.FromTag(prop, (ushort)type);
|
return new SearchQuery.PropertyIdentifier(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PropTag FromInt(int v)
|
public static PropTag FromInt(int v)
|
||||||
@ -65,80 +66,110 @@ namespace Acacia.Native.MAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: align is probably wrong for 32-bit
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
public struct PropValue
|
||||||
unsafe public struct PropValue
|
|
||||||
{
|
{
|
||||||
[FieldOffset(0)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
public PropTag ulPropTag;
|
public struct Header
|
||||||
|
{
|
||||||
|
public PropTag ulPropTag;
|
||||||
|
}
|
||||||
|
|
||||||
[FieldOffset(4)]
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
public uint dwAlignPad;
|
unsafe public struct Data
|
||||||
|
{
|
||||||
|
// short int i; /* case PT_I2 */
|
||||||
|
// LONG l; /* case PT_LONG */
|
||||||
|
// ULONG ul; /* alias for PT_LONG */
|
||||||
|
// LPVOID lpv; /* alias for PT_PTR */
|
||||||
|
// float flt; /* case PT_R4 */
|
||||||
|
// double dbl; /* case PT_DOUBLE */
|
||||||
|
// unsigned short int b; /* case PT_BOOLEAN */
|
||||||
|
[FieldOffset(0), MarshalAs(UnmanagedType.U2)]
|
||||||
|
public bool b;
|
||||||
|
|
||||||
// short int i; /* case PT_I2 */
|
// CURRENCY cur; /* case PT_CURRENCY */
|
||||||
// LONG l; /* case PT_LONG */
|
// double at; /* case PT_APPTIME */
|
||||||
// ULONG ul; /* alias for PT_LONG */
|
// FILETIME ft; /* case PT_SYSTIME */
|
||||||
// LPVOID lpv; /* alias for PT_PTR */
|
|
||||||
// float flt; /* case PT_R4 */
|
|
||||||
// double dbl; /* case PT_DOUBLE */
|
|
||||||
// unsigned short int b; /* case PT_BOOLEAN */
|
|
||||||
[FieldOffset(8), MarshalAs(UnmanagedType.U2)]
|
|
||||||
public bool b;
|
|
||||||
|
|
||||||
// CURRENCY cur; /* case PT_CURRENCY */
|
// LPSTR lpszA; /* case PT_STRING8 */
|
||||||
// double at; /* case PT_APPTIME */
|
[FieldOffset(0), MarshalAs(UnmanagedType.LPStr)]
|
||||||
// FILETIME ft; /* case PT_SYSTIME */
|
public sbyte* lpszA;
|
||||||
|
|
||||||
// LPSTR lpszA; /* case PT_STRING8 */
|
// SBinary bin; /* case PT_BINARY */
|
||||||
[FieldOffset(8), MarshalAs(UnmanagedType.LPStr)]
|
[FieldOffset(0)]
|
||||||
public sbyte* lpszA;
|
public SBinary bin;
|
||||||
|
|
||||||
// SBinary bin; /* case PT_BINARY */
|
// LPWSTR lpszW; /* case PT_UNICODE */
|
||||||
[FieldOffset(8)]
|
[FieldOffset(0), MarshalAs(UnmanagedType.LPWStr)]
|
||||||
public SBinary bin;
|
public char* lpszW;
|
||||||
|
|
||||||
// LPWSTR lpszW; /* case PT_UNICODE */
|
// LPGUID lpguid; /* case PT_CLSID */
|
||||||
[FieldOffset(8), MarshalAs(UnmanagedType.LPWStr)]
|
// LARGE_INTEGER li; /* case PT_I8 */
|
||||||
public char* lpszW;
|
// SShortArray MVi; /* case PT_MV_I2 */
|
||||||
|
// SLongArray MVl; /* case PT_MV_LONG */
|
||||||
|
// SRealArray MVflt; /* case PT_MV_R4 */
|
||||||
|
// SDoubleArray MVdbl; /* case PT_MV_DOUBLE */
|
||||||
|
// SCurrencyArray MVcur; /* case PT_MV_CURRENCY */
|
||||||
|
// SAppTimeArray MVat; /* case PT_MV_APPTIME */
|
||||||
|
// SDateTimeArray MVft; /* case PT_MV_SYSTIME */
|
||||||
|
// SBinaryArray MVbin; /* case PT_MV_BINARY */
|
||||||
|
// SLPSTRArray MVszA; /* case PT_MV_STRING8 */
|
||||||
|
// SWStringArray MVszW; /* case PT_MV_UNICODE */
|
||||||
|
|
||||||
// LPGUID lpguid; /* case PT_CLSID */
|
// SGuidArray MVguid; /* case PT_MV_CLSID */
|
||||||
// LARGE_INTEGER li; /* case PT_I8 */
|
// SLargeIntegerArray MVli; /* case PT_MV_I8 */
|
||||||
// SShortArray MVi; /* case PT_MV_I2 */
|
// SCODE err; /* case PT_ERROR */
|
||||||
// SLongArray MVl; /* case PT_MV_LONG */
|
// LONG x; /* case PT_NULL, PT_OBJECT (no usable value) */
|
||||||
// SRealArray MVflt; /* case PT_MV_R4 */
|
}
|
||||||
// SDoubleArray MVdbl; /* case PT_MV_DOUBLE */
|
|
||||||
// SCurrencyArray MVcur; /* case PT_MV_CURRENCY */
|
|
||||||
// SAppTimeArray MVat; /* case PT_MV_APPTIME */
|
|
||||||
// SDateTimeArray MVft; /* case PT_MV_SYSTIME */
|
|
||||||
// SBinaryArray MVbin; /* case PT_MV_BINARY */
|
|
||||||
// SLPSTRArray MVszA; /* case PT_MV_STRING8 */
|
|
||||||
// SWStringArray MVszW; /* case PT_MV_UNICODE */
|
|
||||||
|
|
||||||
// SGuidArray MVguid; /* case PT_MV_CLSID */
|
public Header header;
|
||||||
// SLargeIntegerArray MVli; /* case PT_MV_I8 */
|
public Data data;
|
||||||
// SCODE err; /* case PT_ERROR */
|
|
||||||
// LONG x; /* case PT_NULL, PT_OBJECT (no usable value) */
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return ToObject()?.ToString() ?? "<unknown>";
|
return ToObject()?.ToString() ?? "<unknown>";
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ToObject()
|
unsafe public object ToObject()
|
||||||
{
|
{
|
||||||
switch (ulPropTag.type)
|
switch (header.ulPropTag.type)
|
||||||
{
|
{
|
||||||
case PropType.BOOLEAN:
|
case PropType.BOOLEAN:
|
||||||
return b;
|
return data.b;
|
||||||
case PropType.STRING8:
|
case PropType.STRING8:
|
||||||
return new string(lpszA);
|
return new string(data.lpszA);
|
||||||
|
case PropType.UNICODE:
|
||||||
|
return new string(data.lpszW);
|
||||||
case PropType.BINARY:
|
case PropType.BINARY:
|
||||||
return bin;
|
return data.bin;
|
||||||
//case PropType.UNICODE:
|
}
|
||||||
// return lpszW.ToString();
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe public static IntPtr MarshalFromObject(NativeEncoder encoder, PropTag prop, object value)
|
||||||
|
{
|
||||||
|
PropValue obj = new PropValue();
|
||||||
|
obj.header.ulPropTag = prop;
|
||||||
|
|
||||||
|
switch (prop.type)
|
||||||
|
{
|
||||||
|
case PropType.BOOLEAN:
|
||||||
|
obj.data.b = (bool)value;
|
||||||
|
return encoder.Allocate(obj.header, obj.data.b);
|
||||||
|
case PropType.STRING8:
|
||||||
|
IntPtr ptrA = encoder.Allocate(Encoding.ASCII.GetBytes((string)value), new byte[] { 0 });
|
||||||
|
return encoder.Allocate(obj.header, ptrA);
|
||||||
|
case PropType.UNICODE:
|
||||||
|
IntPtr ptrW = encoder.Allocate(Encoding.Unicode.GetBytes((string)value), new byte[] { 0, 0 });
|
||||||
|
return encoder.Allocate(obj.header, ptrW);
|
||||||
|
case PropType.BINARY:
|
||||||
|
obj.data.bin = ((SBinary)value).Marshal(encoder);
|
||||||
|
return encoder.Allocate(obj.header, obj.data.bin);
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
using Acacia.Stubs;
|
using Acacia.Stubs;
|
||||||
|
using Acacia.Utils;
|
||||||
/// Copyright 2017 Kopano b.v.
|
/// 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
|
||||||
@ -69,14 +70,14 @@ namespace Acacia.Native.MAPI
|
|||||||
|
|
||||||
unsafe public struct ContentRestriction
|
unsafe public struct ContentRestriction
|
||||||
{
|
{
|
||||||
public FuzzyLevel fuzzy;
|
public FuzzyLevel ulFuzzyLevel;
|
||||||
public PropTag ulPropTag;
|
public PropTag ulPropTag;
|
||||||
public PropValue* prop;
|
public PropValue* prop;
|
||||||
|
|
||||||
public string ToString(int depth)
|
public string ToString(int depth)
|
||||||
{
|
{
|
||||||
string indent = new string(' ', depth);
|
string indent = new string(' ', depth);
|
||||||
string s = indent + fuzzy + ":" + ulPropTag.ToString();
|
string s = indent + ulFuzzyLevel + ":" + ulPropTag.ToString();
|
||||||
s += ":" + prop->ToString();
|
s += ":" + prop->ToString();
|
||||||
s += "\n";
|
s += "\n";
|
||||||
return s;
|
return s;
|
||||||
@ -85,9 +86,15 @@ namespace Acacia.Native.MAPI
|
|||||||
public SearchQuery ToSearchQuery()
|
public SearchQuery ToSearchQuery()
|
||||||
{
|
{
|
||||||
return new SearchQuery.PropertyContent(ulPropTag.ToPropertyIdentifier(),
|
return new SearchQuery.PropertyContent(ulPropTag.ToPropertyIdentifier(),
|
||||||
(uint)fuzzy, // TODO
|
(SearchQuery.ContentMatchOperation)((uint)ulFuzzyLevel & 0xF),
|
||||||
|
(SearchQuery.ContentMatchModifiers)(((uint)ulFuzzyLevel & 0xF0000) >> 16),
|
||||||
prop->ToObject());
|
prop->ToObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static FuzzyLevel FuzzyLevelFromSearchQuery(SearchQuery.PropertyContent search)
|
||||||
|
{
|
||||||
|
return (FuzzyLevel)((int)search.Operation | ((int)search.Modifiers << 16));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with ISearch
|
// TODO: merge with ISearch
|
||||||
@ -174,7 +181,7 @@ namespace Acacia.Native.MAPI
|
|||||||
|
|
||||||
public SearchQuery ToSearchQuery()
|
public SearchQuery ToSearchQuery()
|
||||||
{
|
{
|
||||||
return new SearchQuery.PropertyBitMask(prop.ToPropertyIdentifier(), bmr == BMR.EQZ, mask);
|
return new SearchQuery.PropertyBitMask(prop.ToPropertyIdentifier(), (SearchQuery.BitMaskOperation)(int)bmr, mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,28 +312,153 @@ namespace Acacia.Native.MAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Example search code
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
unsafe public class RestrictionEncoder : NativeEncoder, ISearchEncoder
|
||||||
{
|
{
|
||||||
MAPIFolder folder = (MAPIFolder)account.Store.GetSpecialFolder(Microsoft.Office.Interop.Outlook.OlSpecialFolders.olSpecialFolderReminders);
|
private class EncodingStack
|
||||||
dynamic obj = folder.MAPIOBJECT;
|
{
|
||||||
IMAPIFolder imapi = obj as IMAPIFolder;
|
public SRestriction[] array;
|
||||||
|
public int index;
|
||||||
|
public SRestriction* ptr;
|
||||||
|
|
||||||
//imapi.GetSearchCriteria(0, IntPtr.Zero, IntPtr.Zero, ref state);
|
public EncodingStack(int count, Allocation<SRestriction[]> alloc)
|
||||||
GetSearchCriteriaState state;
|
{
|
||||||
//imapi.GetContentsTable(0, out p);
|
array = alloc.Object;
|
||||||
SBinaryArray* sb1;
|
index = 0;
|
||||||
SRestriction* restrict;
|
ptr = (SRestriction*)alloc.Pointer;
|
||||||
imapi.GetSearchCriteria(0, &restrict, &sb1, out state);
|
}
|
||||||
Logger.Instance.Warning(this, "SEARCH:\n{0}", restrict->ToString());
|
}
|
||||||
|
private readonly Stack<EncodingStack> _current = new Stack<EncodingStack>();
|
||||||
|
private readonly EncodingStack _root;
|
||||||
|
|
||||||
restrict->rt = RestrictionType.AND;
|
public RestrictionEncoder()
|
||||||
imapi.SetSearchCriteria(restrict, sb1, SetSearchCriteriaFlags.NONE);
|
{
|
||||||
|
// Create an object for the root element
|
||||||
|
_root = Begin(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DoRelease()
|
||||||
|
{
|
||||||
|
base.DoRelease();
|
||||||
|
}
|
||||||
|
|
||||||
//SBinaryArray sb = Marshal.PtrToStructure<SBinaryArray>(p2);
|
public SRestriction Restriction
|
||||||
//byte[][] ids = sb.Unmarshal();
|
{
|
||||||
//Logger.Instance.Warning(this, "SEARCH: {0}", StringUtil.BytesToHex(ids[0]));
|
get { return _root.array[0]; }
|
||||||
//imapi.GetLastError(0, 0, out p2);
|
}
|
||||||
//imapi.SaveChanges(SaveChangesFlags.FORCE_SAVE);
|
|
||||||
} */
|
private SRestriction* Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EncodingStack top = _current.Peek();
|
||||||
|
return top.ptr + top.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Encode(SearchQuery.PropertyExists part)
|
||||||
|
{
|
||||||
|
Current->rt = RestrictionType.EXIST;
|
||||||
|
Current->exist.prop = part.Property.Tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Encode(SearchQuery.Or part)
|
||||||
|
{
|
||||||
|
Current->rt = RestrictionType.OR;
|
||||||
|
Current->sub.cb = (uint)part.Operands.Count;
|
||||||
|
Current->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->rt = RestrictionType.NOT;
|
||||||
|
Current->not.ptr = EncodePointer(new[] { part.Operand });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Encode(SearchQuery.And part)
|
||||||
|
{
|
||||||
|
Current->rt = RestrictionType.AND;
|
||||||
|
Current->sub.cb = (uint)part.Operands.Count;
|
||||||
|
Current->sub.ptr = EncodePointer(part.Operands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SRestriction* EncodePointer(IEnumerable<SearchQuery> 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->rt = RestrictionType.CONTENT;
|
||||||
|
Current->content.ulFuzzyLevel = ContentRestriction.FuzzyLevelFromSearchQuery(part);
|
||||||
|
Current->content.ulPropTag = part.Property.Tag;
|
||||||
|
Current->content.prop = (PropValue*)PropValue.MarshalFromObject(this, part.Property.Tag, part.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Encode(SearchQuery.PropertyCompare part)
|
||||||
|
{
|
||||||
|
Current->rt = RestrictionType.PROPERTY;
|
||||||
|
Current->prop.relop = (SearchOperation)part.Operation;
|
||||||
|
Current->prop.ulPropTag = part.Property.Tag;
|
||||||
|
Current->prop.prop = (PropValue*)PropValue.MarshalFromObject(this, part.Property.Tag, part.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Encode(SearchQuery.PropertyBitMask part)
|
||||||
|
{
|
||||||
|
Current->rt = RestrictionType.BITMASK;
|
||||||
|
Current->bitMask.bmr = (BMR)(int)part.Operation;
|
||||||
|
Current->bitMask.prop = part.Property.Tag;
|
||||||
|
Current->bitMask.mask = part.Mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RestrictionExensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the search as an SRestriction.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The encoder containing the restriction. The caller is responsible for disposing.</returns>
|
||||||
|
public static RestrictionEncoder ToRestriction(this SearchQuery search)
|
||||||
|
{
|
||||||
|
RestrictionEncoder encoder = new RestrictionEncoder();
|
||||||
|
search.Encode(encoder);
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Acacia.Native.MAPI;
|
||||||
|
|
||||||
namespace Acacia.Native
|
namespace Acacia.Native
|
||||||
{
|
{
|
||||||
@ -13,7 +14,7 @@ namespace Acacia.Native
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
abstract public class NativeEncoder : DisposableWrapper
|
abstract public class NativeEncoder : DisposableWrapper
|
||||||
{
|
{
|
||||||
abstract protected class AllocationBase
|
protected class AllocationBase : IDisposable
|
||||||
{
|
{
|
||||||
protected readonly object _obj;
|
protected readonly object _obj;
|
||||||
protected readonly GCHandle _handle;
|
protected readonly GCHandle _handle;
|
||||||
@ -31,15 +32,24 @@ namespace Acacia.Native
|
|||||||
_ptr = _handle.AddrOfPinnedObject();
|
_ptr = _handle.AddrOfPinnedObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: release
|
public IntPtr Pointer { get { return _ptr; } }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_handle.IsAllocated)
|
||||||
|
_handle.Free();
|
||||||
|
else
|
||||||
|
Marshal.FreeHGlobal(_ptr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe protected class Allocation<ObjType> : AllocationBase
|
unsafe protected class Allocation<ObjType> : AllocationBase
|
||||||
{
|
{
|
||||||
public Allocation(int size) : base(typeof(ObjType), size)
|
internal Allocation(int size) : base(typeof(ObjType), size)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Allocation(ObjType obj) : base(obj)
|
internal Allocation(ObjType obj) : base(obj)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,26 +60,21 @@ namespace Acacia.Native
|
|||||||
return (ObjType)_obj;
|
return (ObjType)_obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IntPtr Pointer
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _ptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<AllocationBase> _allocs = new List<AllocationBase>();
|
private readonly List<AllocationBase> _allocs = new List<AllocationBase>();
|
||||||
|
|
||||||
/// <summary>
|
override protected void DoRelease()
|
||||||
/// Allocates an object of the specified type. The allocation is managed by this encoder.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="size">If larger than 0, the size to allocate. Otherwise, the size of the object is used.</param>
|
|
||||||
/// <returns>The allocated object.</returns>
|
|
||||||
protected Allocation<ObjType> Allocate<ObjType>(int size = -1)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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<ObjType> Allocate<ObjType>(ObjType obj)
|
protected Allocation<ObjType> Allocate<ObjType>(ObjType obj)
|
||||||
@ -78,5 +83,80 @@ namespace Acacia.Native
|
|||||||
_allocs.Add(alloc);
|
_allocs.Add(alloc);
|
||||||
return alloc;
|
return alloc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Allocation<ObjType> Allocate<ObjType>()
|
||||||
|
{
|
||||||
|
return Allocate<ObjType>(Activator.CreateInstance<ObjType>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>(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<ElementType>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a block of memory containing all specified objects sequentially.
|
||||||
|
/// </summary>
|
||||||
|
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];
|
||||||
|
for (int i = 0; i < all.Length; ++i)
|
||||||
|
{
|
||||||
|
starts[i] = Align(size);
|
||||||
|
int thisSize = Marshal.SizeOf(all[i]);
|
||||||
|
size = starts[i] + thisSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
AllocationBase alloc = Allocate(size);
|
||||||
|
IntPtr ptr = alloc.Pointer;
|
||||||
|
for (int i = 0; i < all.Length; ++i)
|
||||||
|
{
|
||||||
|
Marshal.StructureToPtr(all[i], ptr + starts[i], false);
|
||||||
|
}
|
||||||
|
return alloc.Pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private int Align(int size)
|
||||||
|
{
|
||||||
|
int align = Marshal.SizeOf<IntPtr>();
|
||||||
|
int additional = (align - (size % align)) % align;
|
||||||
|
return size + additional;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ namespace Acacia
|
|||||||
void Encode(SearchQuery.And part);
|
void Encode(SearchQuery.And part);
|
||||||
void Encode(SearchQuery.Or part);
|
void Encode(SearchQuery.Or part);
|
||||||
void Encode(SearchQuery.Not part);
|
void Encode(SearchQuery.Not part);
|
||||||
|
void Encode(SearchQuery.PropertyIdentifier part);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ToStringEncoder : ISearchEncoder
|
public class ToStringEncoder : ISearchEncoder
|
||||||
@ -46,21 +47,20 @@ namespace Acacia
|
|||||||
|
|
||||||
public void Encode(SearchQuery.And part)
|
public void Encode(SearchQuery.And part)
|
||||||
{
|
{
|
||||||
EncodeMulti(part, "AND");
|
EncodeMulti("AND", part.Operands);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Encode(SearchQuery.Or part)
|
public void Encode(SearchQuery.Or part)
|
||||||
{
|
{
|
||||||
EncodeMulti(part, "OR");
|
EncodeMulti("OR", part.Operands);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Encode(SearchQuery.Not part)
|
public void Encode(SearchQuery.Not part)
|
||||||
{
|
{
|
||||||
_builder.Append("NOT ");
|
EncodeMulti("NOT", new[] { part.Operand });
|
||||||
part.Operand.Encode(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EncodeMulti(SearchQuery.MultiOperator part, string oper)
|
private void EncodeMulti(string oper, IEnumerable<SearchQuery> parts)
|
||||||
{
|
{
|
||||||
Indent();
|
Indent();
|
||||||
_builder.Append(oper).Append("\n");
|
_builder.Append(oper).Append("\n");
|
||||||
@ -69,7 +69,7 @@ namespace Acacia
|
|||||||
|
|
||||||
++_indent;
|
++_indent;
|
||||||
|
|
||||||
foreach (SearchQuery operand in part.Operands)
|
foreach (SearchQuery operand in parts)
|
||||||
operand.Encode(this);
|
operand.Encode(this);
|
||||||
|
|
||||||
--_indent;
|
--_indent;
|
||||||
@ -80,22 +80,59 @@ namespace Acacia
|
|||||||
|
|
||||||
public void Encode(SearchQuery.PropertyBitMask part)
|
public void Encode(SearchQuery.PropertyBitMask part)
|
||||||
{
|
{
|
||||||
_builder.Append("BITMASK:").Append(part.Property); // TODO: operator/value
|
Indent();
|
||||||
|
_builder.Append("BITMASK{");
|
||||||
|
part.Property.Encode(this);
|
||||||
|
_builder.Append(" ").Append(part.Operation).Append(" ");
|
||||||
|
_builder.Append(part.Mask.ToString("X8"));
|
||||||
|
_builder.Append("}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string[] COMPARISON_OPERATORS = {"<", "<=", ">", ">=", "==", "!=", "LIKE"};
|
||||||
|
|
||||||
public void Encode(SearchQuery.PropertyCompare part)
|
public void Encode(SearchQuery.PropertyCompare part)
|
||||||
{
|
{
|
||||||
_builder.Append("COMPARE:").Append(part.Property); // TODO: operator/value
|
Indent();
|
||||||
|
_builder.Append("COMPARE{");
|
||||||
|
part.Property.Encode(this);
|
||||||
|
_builder.Append(" ").Append(COMPARISON_OPERATORS[(int)part.Operation]).Append(" ");
|
||||||
|
_builder.Append(part.Value);
|
||||||
|
_builder.Append("}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Encode(SearchQuery.PropertyContent part)
|
public void Encode(SearchQuery.PropertyContent part)
|
||||||
{
|
{
|
||||||
_builder.Append("CONTENT:").Append(part.Property); // TODO: operator/value
|
Indent();
|
||||||
|
_builder.Append("CONTENT{");
|
||||||
|
part.Property.Encode(this);
|
||||||
|
|
||||||
|
List<string> options = new List<string>();
|
||||||
|
if (part.Operation != SearchQuery.ContentMatchOperation.Full)
|
||||||
|
options.Add(part.Operation.ToString());
|
||||||
|
|
||||||
|
if (part.Modifiers != SearchQuery.ContentMatchModifiers.None)
|
||||||
|
{
|
||||||
|
options.Add(part.Modifiers.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
string optionsString = options.Count == 0 ? "" : ("(" + string.Join(",", options) + ")");
|
||||||
|
|
||||||
|
_builder.Append(" ==").Append(optionsString).Append(" ");
|
||||||
|
_builder.Append(part.Content);
|
||||||
|
_builder.Append("}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Encode(SearchQuery.PropertyExists part)
|
public void Encode(SearchQuery.PropertyExists part)
|
||||||
{
|
{
|
||||||
_builder.Append("EXISTS:").Append(part.Property);
|
Indent();
|
||||||
|
_builder.Append("EXISTS{");
|
||||||
|
part.Property.Encode(this);
|
||||||
|
_builder.Append("}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Encode(SearchQuery.PropertyIdentifier part)
|
||||||
|
{
|
||||||
|
_builder.Append(part.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetValue()
|
public string GetValue()
|
||||||
@ -133,7 +170,7 @@ namespace Acacia
|
|||||||
_operands.Add(operand);
|
_operands.Add(operand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<SearchQuery> Operands
|
public ICollection<SearchQuery> Operands
|
||||||
{
|
{
|
||||||
get { return _operands; }
|
get { return _operands; }
|
||||||
}
|
}
|
||||||
@ -180,16 +217,23 @@ namespace Acacia
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class PropertyIdentifier
|
public class PropertyIdentifier
|
||||||
{
|
{
|
||||||
private string _id;
|
public string Id { get; private set; }
|
||||||
|
public PropTag Tag { get; private set; }
|
||||||
|
|
||||||
public PropertyIdentifier(string id)
|
public PropertyIdentifier(PropTag tag)
|
||||||
{
|
{
|
||||||
this._id = id;
|
this.Tag = tag;
|
||||||
|
Id = string.Format("{0:X4}{1:X4}", tag.prop, (int)tag.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PropertyIdentifier FromTag(ushort prop, ushort type)
|
public void Encode(ISearchEncoder encoder)
|
||||||
{
|
{
|
||||||
return new PropertyIdentifier(string.Format("{0:4X}{1:4X}", prop, type));
|
encoder.Encode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,11 +291,44 @@ namespace Acacia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ContentMatchOperation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Match full content
|
||||||
|
/// </summary>
|
||||||
|
Full,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match part of the content
|
||||||
|
/// </summary>
|
||||||
|
SubString,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match the start of the content
|
||||||
|
/// </summary>
|
||||||
|
Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum ContentMatchModifiers
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
CaseInsensitive = 1,
|
||||||
|
IgnoreNonSpace = 2,
|
||||||
|
Loose = 4
|
||||||
|
}
|
||||||
|
|
||||||
public class PropertyContent : PropertyQuery
|
public class PropertyContent : PropertyQuery
|
||||||
{
|
{
|
||||||
public PropertyContent(PropertyIdentifier property, uint options, object content) : base(property)
|
public ContentMatchOperation Operation { get; set; }
|
||||||
|
public ContentMatchModifiers Modifiers { get; set; }
|
||||||
|
public object Content { get; set; }
|
||||||
|
|
||||||
|
public PropertyContent(PropertyIdentifier property, ContentMatchOperation operation, ContentMatchModifiers modifiers, object content) : base(property)
|
||||||
{
|
{
|
||||||
// TODO
|
this.Operation = operation;
|
||||||
|
this.Modifiers = modifiers;
|
||||||
|
this.Content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Encode(ISearchEncoder encoder)
|
public override void Encode(ISearchEncoder encoder)
|
||||||
@ -260,11 +337,20 @@ namespace Acacia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum BitMaskOperation
|
||||||
|
{
|
||||||
|
EQZ, NEZ
|
||||||
|
}
|
||||||
|
|
||||||
public class PropertyBitMask : PropertyQuery
|
public class PropertyBitMask : PropertyQuery
|
||||||
{
|
{
|
||||||
public PropertyBitMask(PropertyIdentifier property, bool wantZero, uint mask) : base(property)
|
public BitMaskOperation Operation { get; set; }
|
||||||
|
public uint Mask { get; set; }
|
||||||
|
|
||||||
|
public PropertyBitMask(PropertyIdentifier property, BitMaskOperation operation, uint mask) : base(property)
|
||||||
{
|
{
|
||||||
// TODO
|
this.Operation = operation;
|
||||||
|
this.Mask = mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Encode(ISearchEncoder encoder)
|
public override void Encode(ISearchEncoder encoder)
|
||||||
|
@ -403,7 +403,6 @@ namespace Acacia.Stubs.OutlookWrappers
|
|||||||
SBinaryArray* sb1;
|
SBinaryArray* sb1;
|
||||||
SRestriction* restrict;
|
SRestriction* restrict;
|
||||||
imapi.GetSearchCriteria(0, &restrict, &sb1, out state);
|
imapi.GetSearchCriteria(0, &restrict, &sb1, out state);
|
||||||
Logger.Instance.Warning(this, "SEARCH:\n{0}", restrict->ToString());
|
|
||||||
return restrict->ToSearchQuery();
|
return restrict->ToSearchQuery();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -414,8 +413,19 @@ namespace Acacia.Stubs.OutlookWrappers
|
|||||||
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
// TODO
|
IMAPIFolder imapi = _item.MAPIOBJECT as IMAPIFolder;
|
||||||
throw new NotImplementedException();
|
try
|
||||||
|
{
|
||||||
|
using (RestrictionEncoder res = value.ToRestriction())
|
||||||
|
{
|
||||||
|
SRestriction restrict = res.Restriction;
|
||||||
|
imapi.SetSearchCriteria(&restrict, null, SearchCriteriaFlags.NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ComRelease.Release(imapi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user