diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index b253358..f989cbf 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -244,6 +244,7 @@ + Form @@ -303,6 +304,7 @@ + @@ -320,6 +322,7 @@ + diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs index eebca38..3ad3022 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs @@ -44,6 +44,12 @@ namespace Acacia #endregion + #region Meeting requests + + public const string ZPUSH_MEETING_UID = OutlookConstants.NS_TRANSPORT_MESSAGE_HEADERS + "X-Push-Meeting-UID"; + + #endregion + #region GAB public const string ZPUSH_GAB_INDEX = "$PushIndex"; diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs index fad252a..a3166de 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs @@ -37,6 +37,7 @@ namespace Acacia.Features typeof(SecondaryContacts.FeatureSecondaryContacts), typeof(SendAs.FeatureSendAs), typeof(Signatures.FeatureSignatures), + typeof(MeetingRequest.FeatureMeetingRequest), typeof(DebugSupport.FeatureDebugSupport) }; } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/MeetingRequest/FeatureMeetingRequest.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/MeetingRequest/FeatureMeetingRequest.cs new file mode 100644 index 0000000..c821f41 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/MeetingRequest/FeatureMeetingRequest.cs @@ -0,0 +1,53 @@ +/// Copyright 2017 Kopano b.v. +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License, version 3, +/// as published by the Free Software Foundation. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program.If not, see. +/// +/// Consult LICENSE file for details + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Acacia.Stubs; +using Acacia.Utils; +using Acacia.ZPush; +using Acacia.Features.SharedFolders; +using Acacia.ZPush.API.SharedFolders; +using static Acacia.DebugOptions; + +namespace Acacia.Features.MeetingRequest +{ + [AcaciaOption("Provides the ability to select different senders for Z-Push accounts.")] + public class FeatureMeetingRequest : Feature + { + public FeatureMeetingRequest() + { + } + + public override void Startup() + { + if (MailEvents != null) + { + MailEvents.ItemSend.Register(Meeting_ItemSend); + } + } + + private void Meeting_ItemSend(IMeetingItem item, ref bool cancel) + { + byte[] uid = item.GlobalObjectId; + item.SetProperty(Constants.ZPUSH_MEETING_UID, uid.BytesToHex()); + item.Save(); + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs index 912a597..ccaf26c 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs @@ -51,7 +51,7 @@ namespace Acacia.Features.SendAs { if (MailEvents != null) { - MailEvents.ItemSend += MailEvents_ItemSend; + MailEvents.ItemSend.Register(MailEvents_ItemSend); } if (SendAsOwner) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs index c7f9000..f6f6e9b 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs @@ -114,6 +114,13 @@ namespace Acacia #endregion + #region Meeting requests + + public const string PSETID_MEETING = GUID + "{6ED8DA90-450B-101B-98DA-00AA003F1305}/"; + public const string PR_MEETING_UID = PSETID_MEETING + "0003" + PT_BINARY; + + #endregion + #region EAS / ZPush public const string PR_ZPUSH_MESSAGE_ID = PROP + "6B20" + PT_STRING8; @@ -229,12 +236,12 @@ namespace Acacia #region Notes - public const string PREFIX_NOTES = GUID + "{0006200E-0000-0000-C000-000000000046}/"; - public const string PR_NOTE_COLOR = PREFIX_NOTES + "8B00" + PT_LONG; - public const string PR_NOTE_WIDTH = PREFIX_NOTES + "8B02" + PT_LONG; - public const string PR_NOTE_HEIGHT = PREFIX_NOTES + "8B03" + PT_LONG; - public const string PR_NOTE_X = PREFIX_NOTES + "8B04" + PT_LONG; - public const string PR_NOTE_Y = PREFIX_NOTES + "8B05" + PT_LONG; + public const string PSETID_NOTE = GUID + "{0006200E-0000-0000-C000-000000000046}/"; + public const string PR_NOTE_COLOR = PSETID_NOTE + "8B00" + PT_LONG; + public const string PR_NOTE_WIDTH = PSETID_NOTE + "8B02" + PT_LONG; + public const string PR_NOTE_HEIGHT = PSETID_NOTE + "8B03" + PT_LONG; + public const string PR_NOTE_X = PSETID_NOTE + "8B04" + PT_LONG; + public const string PR_NOTE_Y = PSETID_NOTE + "8B05" + PT_LONG; #endregion diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAppointmentItem.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAppointmentItem.cs index dde7298..3f088da 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAppointmentItem.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAppointmentItem.cs @@ -30,5 +30,6 @@ namespace Acacia.Stubs DateTime Start { get; set; } DateTime End { get; set; } string Location { get; set; } + string GlobalAppointmentId { get; } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMeetingItem.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMeetingItem.cs new file mode 100644 index 0000000..16427ec --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMeetingItem.cs @@ -0,0 +1,38 @@ +/// Copyright 2017 Kopano b.v. +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License, version 3, +/// as published by the Free Software Foundation. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program.If not, see. +/// +/// Consult LICENSE file for details + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Stubs +{ + /// + /// Specialisation for meeting requests + /// + public interface IMeetingItem : IItem + { + IAppointmentItem GetAssociatedAppointment(bool addToCalendar); + + byte[] GlobalObjectId + { + get; + set; + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AppointmentItemWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AppointmentItemWrapper.cs index 46ed301..588ce90 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AppointmentItemWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AppointmentItemWrapper.cs @@ -52,6 +52,11 @@ namespace Acacia.Stubs.OutlookWrappers set { _item.Location = value; } } + public string GlobalAppointmentId + { + get { return _item.GlobalAppointmentID; } + } + #endregion #region Wrapper methods diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/Mapping.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/Mapping.cs index 3aae7e4..8596828 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/Mapping.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/Mapping.cs @@ -62,6 +62,8 @@ namespace Acacia.Stubs.OutlookWrappers return new NoteItemWrapper((NSOutlook.NoteItem)o); if (o is NSOutlook.TaskItem) return new TaskItemWrapper((NSOutlook.TaskItem)o); + if (o is NSOutlook.MeetingItem) + return new MeetingItemWrapper((NSOutlook.MeetingItem)o); // TODO: support others? if (mustRelease) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MeetingItemWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MeetingItemWrapper.cs new file mode 100644 index 0000000..334385e --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MeetingItemWrapper.cs @@ -0,0 +1,164 @@ +/// Copyright 2017 Kopano b.v. +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License, version 3, +/// as published by the Free Software Foundation. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program.If not, see. +/// +/// Consult LICENSE file for details + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Acacia.Utils; +using NSOutlook = Microsoft.Office.Interop.Outlook; + +namespace Acacia.Stubs.OutlookWrappers +{ + class MeetingItemWrapper : OutlookItemWrapper, IMeetingItem + { + + internal MeetingItemWrapper(NSOutlook.MeetingItem item) + : + base(item) + { + } + + public IAppointmentItem GetAssociatedAppointment(bool addToCalendar) + { + return _item.GetAssociatedAppointment(addToCalendar).Wrap(); + } + + public byte[] GlobalObjectId + { + get + { + byte[] uid = (byte[])GetProperty(OutlookConstants.PR_MEETING_UID); + if (uid != null) + return null; + + using (IAppointmentItem appointment = GetAssociatedAppointment(false)) + { + if (appointment != null) + return appointment.GlobalAppointmentId.HexToBytes(); + } + return null; + } + + set + { + SetProperty(OutlookConstants.PR_MEETING_UID, value); + } + } + + #region Wrapper methods + + protected override NSOutlook.UserProperties GetUserProperties() + { + return _item.UserProperties; + } + + protected override NSOutlook.PropertyAccessor GetPropertyAccessor() + { + return _item.PropertyAccessor; + } + + public override string ToString() + { + return "Appointment:" + Subject; + } + + #endregion + + #region IItem implementation + + public string Body + { + get { return _item.Body; } + set { _item.Body = value; } + } + + public string Subject + { + get { return _item.Subject; } + set { _item.Subject = value; } + } + + public void Save() { _item.Save(); } + + #endregion + + #region IBase implementation + + public string EntryID { get { return _item.EntryID; } } + + public IFolder Parent + { + get + { + // The wrapper manages the returned folder + return Mapping.Wrap(_item.Parent as NSOutlook.Folder); + } + } + + public string ParentEntryID + { + get + { + using (ComRelease com = new ComRelease()) + { + NSOutlook.Folder parent = com.Add(_item.Parent); + return parent?.EntryID; + } + } + } + + public IStore GetStore() + { + using (ComRelease com = new ComRelease()) + { + NSOutlook.Folder parent = com.Add(_item.Parent); + return Mapping.Wrap(parent?.Store); + } + } + + public string StoreID + { + get + { + using (ComRelease com = new ComRelease()) + { + NSOutlook.Folder parent = com.Add(_item.Parent); + NSOutlook.Store store = com.Add(parent?.Store); + return store.StoreID; + } + } + } + + public string StoreDisplayName + { + get + { + using (ComRelease com = new ComRelease()) + { + NSOutlook.Folder parent = com.Add(_item.Parent); + NSOutlook.Store store = com.Add(parent?.Store); + return store.StoreID; + } + } + } + + public void Delete() { _item.Delete(); } + + #endregion + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/MailEvents.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/MailEvents.cs index 952889b..455f640 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/MailEvents.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/MailEvents.cs @@ -21,6 +21,7 @@ using System.Text; using System.Threading.Tasks; using Acacia.Stubs; using Acacia.Stubs.OutlookWrappers; +using System.Reflection; namespace Acacia.Utils { @@ -151,26 +152,177 @@ namespace Acacia.Utils } } - public event CancellableMailItemEventHandler ItemSend; - private void OnItemSend(object item, ref bool cancel) + #region Send + + private class Dispatchers { - try + public class Dispatcher { - if (ItemSend != null && item != null) + private readonly Dispatchers _dispatchers; + private bool _failed; + private List _params = new List(); + + public Dispatcher(Dispatchers dispatchers) { - using (IMailItem wrapped = Mapping.WrapOrDefault(item, false)) + this._dispatchers = dispatchers; + } + + public Dispatcher Item(object item, bool mustRelease = false) + { + if (_failed || !_dispatchers.IsRegistered) + return this; + + try { - if (wrapped != null) - ItemSend(wrapped, ref cancel); + IItem wrapped = Mapping.WrapOrDefault(item, mustRelease); + if (wrapped == null) + _failed = true; + else + _params.Add(wrapped); + } + catch (System.Exception e) + { + Logger.Instance.Error(this, "Dispatcher.Item: {0}: {1}", _dispatchers._name, e); + _failed = true; + } + return this; + } + + public void Exec() + { + try + { + ExecInternal(0); + } + catch (System.Exception e) + { + Logger.Instance.Error(this, "Dispatcher.Exec: {0}: {1}", _dispatchers._name, e); + _failed = true; } } + + private object[] ExecInternal(int skipTypeCheck) + { + if (_failed || !_dispatchers.IsRegistered) + return null; + + object[] paramsArray = this._params.ToArray(); + + foreach (Delegate handler in _dispatchers._handlers) + { + // Check the signature + ParameterInfo[] parameters = handler.Method.GetParameters(); + if (parameters.Length != paramsArray.Length) + continue; + + bool invoke = true; + for (int i = 0; i < paramsArray.Length - skipTypeCheck; ++i) + { + // TODO: this doesn't handle null correctly + Type formal = parameters[i].ParameterType; + Type actual = paramsArray[i].GetType(); + if (!formal.IsAssignableFrom(actual)) + { + invoke = false; + break; + } + } + if (!invoke) + continue; + + // Invoke + handler.DynamicInvoke(paramsArray); + } + Cleanup(); + return paramsArray; + } + + public void Exec(ref bool cancel) + { + try + { + _params.Add(cancel); + object[] paramsArray = ExecInternal(1); + if (paramsArray == null) + return; + cancel = (bool)paramsArray.Last(); + _params.RemoveAt(_params.Count - 1); + } + catch (System.Exception e) + { + Logger.Instance.Error(this, "Dispatcher.Exec(cancel): {0}: {1}", _dispatchers._name, e); + _failed = true; + } + } + + private void Cleanup() + { + foreach (object param in _params) + if (param is IDisposable) + ((IDisposable)param).Dispose(); + } } - catch (System.Exception e) + + private List _handlers = new List(); + private readonly string _name; + + private bool IsRegistered { get { return _handlers.Count > 0; } } + + public Dispatchers(string name) { - Logger.Instance.Error(this, "OnItemSend: {0}", e); + this._name = name; + } + + public void Add(Delegate o) + { + _handlers.Add(o); + } + + public void Remove(Delegate o) + { + _handlers.Remove(o); + } + + public Dispatcher Dispatch() + { + return new Dispatcher(this); } } + public class CancellableItemEvent + { + public delegate void Handler(ItemType item, ref bool cancel) + where ItemType : IItem; + + private readonly Dispatchers _handlers; + + public CancellableItemEvent(string name) + { + _handlers = new Dispatchers(name); + } + + public void Register(Handler handler) + where ItemType : IItem + { + _handlers.Add(handler); + } + + public void Unregister(Handler handler) + where ItemType : IItem + { + _handlers.Remove(handler); + } + + internal void Dispatch(object item, ref bool cancel) + { + _handlers.Dispatch().Item(item).Exec(ref cancel); + } + } + + public readonly CancellableItemEvent ItemSend = new CancellableItemEvent("ItemSend"); + + #endregion + #endregion #region Implementation @@ -178,7 +330,7 @@ namespace Acacia.Utils public MailEvents(IAddIn app) { app.ItemLoad += OnItemLoad; - app.ItemSend += OnItemSend; + app.ItemSend += ItemSend.Dispatch; } private void OnItemLoad(object item) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs index 131332b..6801f7c 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs @@ -54,7 +54,7 @@ namespace Acacia.Utils #region Hex strings - public static byte[] HexToBytes(string hex) + public static byte[] HexToBytes(this string hex) { int NumberChars = hex.Length; byte[] bytes = new byte[NumberChars / 2]; @@ -63,14 +63,14 @@ namespace Acacia.Utils return bytes; } - public static string BytesToHex(byte[] bytes) + public static string BytesToHex(this byte[] bytes) { if (bytes == null) return null; return BitConverter.ToString(bytes).Replace("-", ""); } - public static string HexToUtf8(string s) + public static string HexToUtf8(this string s) { return Encoding.UTF8.GetString(HexToBytes(s)); }