diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index 1f3a556..b253358 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -241,6 +241,9 @@ + + + Form @@ -309,6 +312,7 @@ + @@ -323,6 +327,7 @@ + diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/BCC/FeatureBCC.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/BCC/FeatureBCC.cs new file mode 100644 index 0000000..1d7b615 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/BCC/FeatureBCC.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Acacia.Stubs; +using static Acacia.DebugOptions; +using Acacia.ZPush; +using System.Text.RegularExpressions; +using System.Net.Mail; +using Acacia.Utils; + +namespace Acacia.Features.BCC +{ + [AcaciaOption("Displays the BCC field on sent items.")] + public class FeatureBCC : Feature + { + private readonly FolderRegistration _folderRegistration; + + public FeatureBCC() + { + _folderRegistration = new FolderRegistrationDefault(this, DefaultFolder.SentMail); + } + + public override void Startup() + { + // TODO: this is very similar to ReplyFlags + + if (UpdateEvents) + { + // Watch the sent mail folder + Watcher.WatchFolder(_folderRegistration, + (folder) => Watcher.WatchItems(folder, CheckBCC, false) + ); + } + + if (ReadEvent) + { + // As a fallback, add an event handler to update the message when displaying it + if (MailEvents != null) + { + MailEvents.Read += (mail) => + { + // Check we're in the SentMail folder + using (IFolder folder = mail.Parent) + { + if (_folderRegistration.IsApplicable(folder)) + CheckBCC(mail); + } + }; + } + } + } + + private static readonly Regex RE_BCC = new Regex("(?m)^Bcc:[ \t]*(([^\r\n]|\r\n[ \t]+)*)\r\n"); + private static readonly Regex RE_BCC_NAME_EMAIL = new Regex("([^<>]*)[ \t]*<(.*)>"); + + private void CheckBCC(IMailItem mail) + { + // If the item already has a BCC, assume it's correct + if (!string.IsNullOrEmpty(mail.BCC)) + return; + + // Grab the transport headers + string headers = (string)mail.GetProperty(OutlookConstants.PR_TRANSPORT_MESSAGE_HEADERS); + if (string.IsNullOrEmpty(headers)) + return; + + // Check if there's a bcc header + Match match = RE_BCC.Match(headers); + if (match.Groups.Count < 2) + return; + string bcc = match.Groups[1].Value; + if (string.IsNullOrEmpty(bcc)) + return; + + // Add the recipient + string decoded = bcc.DecodeQuotedPrintable(); + try + { + using (IRecipients recipients = mail.Recipients) + { + using (IRecipient recip = CreateRecipient(recipients, decoded)) + { + recip.Type = MailRecipientType.BCC; + } + } + } + finally + { + mail.Save(); + } + } + + private IRecipient CreateRecipient(IRecipients recipients, string decoded) + { + // First try to resolve directly + IRecipient recipient = recipients.Add(decoded); + if (recipient.Resolve()) + return recipient; + + // Nope, remove and create with email + recipient.Dispose(); + recipient = null; + recipients.Remove(recipients.Count - 1); + + string displayName; + string email = ParseBCCHeader(decoded, out displayName); + + // TODO: is it possible to use the display name? + recipient = recipients.Add(email); + recipient.Resolve(); + return recipient; + } + + // TODO: this is probably generally useful + private string ParseBCCHeader(string bcc, out string displayName) + { + Match match = RE_BCC_NAME_EMAIL.Match(bcc); + if (match.Groups.Count > 1) + { + displayName = match.Groups[1].Value; + return match.Groups[2].Value; + } + else + { + displayName = null; + return bcc; + } + } + + #region Debug options + + [AcaciaOption("Enables or disables the handling of read events on mail items. If this is enabled, " + + "the BCC field is checked. This is almost guaranteed to work, but has the downside " + + "of only setting the BCC field when an email is opened.")] + public bool ReadEvent + { + get { return GetOption(OPTION_READ_EVENT); } + set { SetOption(OPTION_READ_EVENT, value); } + } + private static readonly BoolOption OPTION_READ_EVENT = new BoolOption("ReadEvents", true); + + [AcaciaOption("Enables or disables the handling of update events to mail items. When a mail item is " + + "updated, it is checked to see if the BCC field needs to be set. This is the main " + + "mechanism for setting the BCC field.")] + public bool UpdateEvents + { + get { return GetOption(OPTION_UPDATE_EVENTS); } + set { SetOption(OPTION_UPDATE_EVENTS, value); } + } + private static readonly BoolOption OPTION_UPDATE_EVENTS = new BoolOption("FolderEvents", true); + + #endregion + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs index 1b8c6a3..fad252a 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs @@ -27,6 +27,7 @@ namespace Acacia.Features public static readonly Type[] FEATURES = { typeof(ReplyFlags.FeatureReplyFlags), + typeof(BCC.FeatureBCC), typeof(OutOfOffice.FeatureOutOfOffice), typeof(SharedFolders.FeatureSharedFolders), typeof(WebApp.FeatureWebApp), diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs index 3601e42..869ef06 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs @@ -111,9 +111,9 @@ namespace Acacia.Features.FreeBusy private const string REG_KEY = @"Options\Calendar\Internet Free/Busy"; private const string REG_VALUE = @"Read URL"; - internal const string URL_IDENTIFIER = "/zpush/"; + internal const string URL_IDENTIFIER = "zpush"; private const int DEFAULT_PORT = 18632; - private const string URL_PREFIX = @"http://127.0.0.1:{0}" + URL_IDENTIFIER; + private const string URL_PREFIX = @"http://127.0.0.1:{0}/" + URL_IDENTIFIER + "/"; private const string URL = URL_PREFIX + "%NAME%@%SERVER%"; private void Worker() @@ -125,8 +125,9 @@ namespace Acacia.Features.FreeBusy { if (key != null) { + // Set only if empty or already our URL string oldURL = key.GetValueString(REG_VALUE); - if (string.IsNullOrWhiteSpace(oldURL) || oldURL.Contains(URL_IDENTIFIER)) + if (string.IsNullOrWhiteSpace(oldURL) || oldURL.Contains("/" + URL_IDENTIFIER + "/")) key.SetValue(REG_VALUE, string.Format(URL, Port)); } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs index e5265c0..9ba69c2 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs @@ -32,15 +32,15 @@ namespace Acacia.Features.FreeBusy { public class FreeBusyServer { - private readonly FeatureFreeBusy _freeBusy; - private readonly int _port; private readonly Regex _httpRequest; + public delegate Servlet ServletFactory(); + private readonly Dictionary _servlets = new Dictionary(); + public FreeBusyServer(FeatureFreeBusy freeBusy) { - this._freeBusy = freeBusy; - this._port = freeBusy.Port; - this._httpRequest = new Regex(@"^GET " + FeatureFreeBusy.URL_IDENTIFIER + @"([^ ]+) HTTP/(\d.\d)$"); + this._httpRequest = new Regex(@"^GET /([^/]+)/([^ ]*) HTTP/(\d.\d)$"); + _servlets.Add(FeatureFreeBusy.URL_IDENTIFIER, () => new FreeBusyServlet(freeBusy)); } public void HandleRequest(TcpClient client) @@ -61,19 +61,17 @@ namespace Acacia.Features.FreeBusy Logger.Instance.Trace(this, "Invalid request: {0}", s); throw new InvalidOperationException(); } - string username = m.Groups[1].Value; - Logger.Instance.Trace(this, "REQUEST: {0} -> {1}, {2}", s, m.Groups[1], m.Groups[2]); - - // Headers - for (;;) + string app = m.Groups[1].Value; + ServletFactory factory; + if (!_servlets.TryGetValue(app, out factory)) { - s = reader.ReadLine(); - if (string.IsNullOrEmpty(s)) - break; + Logger.Instance.Trace(this, "Unknown servlet: {0} -> {1}", s, app); + throw new InvalidOperationException(); } - // Write response - FetchData(username, writer); + Servlet servlet = factory(); + servlet.Init(s, m.Groups[2].Value, reader, writer); + servlet.Process(); } catch (InvalidOperationException) { @@ -92,67 +90,5 @@ namespace Acacia.Features.FreeBusy Logger.Instance.Error(this, "Error in FreeBusy worker: {0}", e); } } - - private void FetchData(string username, StreamWriter output) - { - // Request the data from the ZPush server - ZPushConnection connection = new ZPushConnection(_freeBusy.FindZPushAccount(username), new System.Threading.CancellationToken(false)); - - // Include yesterday in the request, outlook shows it by default - var request = new ActiveSync.ResolveRecipientsRequest(username, - DateTime.Today.AddDays(-1), - DateTime.Today.AddMonths(6)); - var response = connection.Execute(request); - - // If there is no FreeBusy data, return 404 - if (response?.FreeBusy == null) - { - throw new InvalidOperationException(); - } - - Logger.Instance.Trace(this, "Writing response"); - // Encode the response in vcard format - output.WriteLine("HTTP/1.0 200 OK"); - output.WriteLine("Content-Type: text/vcard"); - output.WriteLine("Connection: close"); - output.WriteLine(""); - - - output.WriteLine("BEGIN:VCALENDAR"); - output.WriteLine("PRODID:-//ZPush//EN"); - output.WriteLine("VERSION:2.0"); - output.WriteLine("BEGIN:VFREEBUSY"); - output.WriteLine("ORGANIZER:" + username); - output.WriteLine(string.Format("URL:http://127.0.0.1:{0}{1}{2}", _port, FeatureFreeBusy.URL_IDENTIFIER, username)); - output.WriteLine(string.Format("DTSTAMP:{0:" + Constants.DATE_ISO_8601 + "}", DateTime.Now)); - output.WriteLine(string.Format("DTSTART:{0:" + Constants.DATE_ISO_8601 + "}", response.FreeBusy.StartTime)); - output.WriteLine(string.Format("DTEND:{0:" + Constants.DATE_ISO_8601 + "}", response.FreeBusy.EndTime)); - - foreach(ActiveSync.FreeBusyData data in response.FreeBusy) - { - if (data.Type != ActiveSync.FreeBusyType.Free) - { - string freeBusy = string.Format("FREEBUSY;FBTYPE={2}:{0:" + Constants.DATE_ISO_8601 + "}/{1:" + Constants.DATE_ISO_8601 + "}", - data.Start, data.End, MapType(data.Type)); - output.WriteLine(freeBusy); - } - } - - output.WriteLine("END:VFREEBUSY"); - output.WriteLine("END:VCALENDAR"); - } - - private object MapType(ActiveSync.FreeBusyType type) - { - switch(type) - { - case ActiveSync.FreeBusyType.Free: return "FREE"; - case ActiveSync.FreeBusyType.Busy: return "BUSY"; - case ActiveSync.FreeBusyType.Tentative: return "BUSY-TENTATIVE"; - case ActiveSync.FreeBusyType.OutOfOffice: return "BUSY-UNAVAILABLE"; - default: - return "BUSY"; - } - } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServlet.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServlet.cs new file mode 100644 index 0000000..8bed6c2 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServlet.cs @@ -0,0 +1,84 @@ +using Acacia.Utils; +using Acacia.ZPush.Connect; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Features.FreeBusy +{ + public class FreeBusyServlet : Servlet + { + private readonly FeatureFreeBusy _freeBusy; + + public FreeBusyServlet(FeatureFreeBusy freeBusy) + { + this._freeBusy = freeBusy; + } + + protected override void ProcessRequest() + { + string username = Url; + // Request the data from the ZPush server + ZPushConnection connection = new ZPushConnection(_freeBusy.FindZPushAccount(username), new System.Threading.CancellationToken(false)); + + // Include yesterday in the request, outlook shows it by default + var request = new ActiveSync.ResolveRecipientsRequest(username, + DateTime.Today.AddDays(-1), + DateTime.Today.AddMonths(6)); + var response = connection.Execute(request); + + // If there is no FreeBusy data, return 404 + if (response?.FreeBusy == null) + { + throw new InvalidOperationException(); + } + + Logger.Instance.Trace(this, "Writing response"); + // Encode the response in vcard format + Out.WriteLine("HTTP/1.0 200 OK"); + Out.WriteLine("Content-Type: text/vcard"); + Out.WriteLine("Connection: close"); + Out.WriteLine(""); + + + Out.WriteLine("BEGIN:VCALENDAR"); + Out.WriteLine("PRODID:-//ZPush//EN"); + Out.WriteLine("VERSION:2.0"); + Out.WriteLine("BEGIN:VFREEBUSY"); + Out.WriteLine("ORGANIZER:" + username); + Out.WriteLine(string.Format("URL:http://127.0.0.1:{0}/{1}/{2}", _freeBusy.Port, FeatureFreeBusy.URL_IDENTIFIER, username)); + Out.WriteLine(string.Format("DTSTAMP:{0:" + Constants.DATE_ISO_8601 + "}", DateTime.Now)); + Out.WriteLine(string.Format("DTSTART:{0:" + Constants.DATE_ISO_8601 + "}", response.FreeBusy.StartTime)); + Out.WriteLine(string.Format("DTEND:{0:" + Constants.DATE_ISO_8601 + "}", response.FreeBusy.EndTime)); + + foreach (ActiveSync.FreeBusyData data in response.FreeBusy) + { + if (data.Type != ActiveSync.FreeBusyType.Free) + { + string freeBusy = string.Format("FREEBUSY;FBTYPE={2}:{0:" + Constants.DATE_ISO_8601 + "}/{1:" + Constants.DATE_ISO_8601 + "}", + data.Start, data.End, MapType(data.Type)); + Out.WriteLine(freeBusy); + } + } + + Out.WriteLine("END:VFREEBUSY"); + Out.WriteLine("END:VCALENDAR"); + } + + private object MapType(ActiveSync.FreeBusyType type) + { + switch (type) + { + case ActiveSync.FreeBusyType.Free: return "FREE"; + case ActiveSync.FreeBusyType.Busy: return "BUSY"; + case ActiveSync.FreeBusyType.Tentative: return "BUSY-TENTATIVE"; + case ActiveSync.FreeBusyType.OutOfOffice: return "BUSY-UNAVAILABLE"; + default: + return "BUSY"; + } + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/Servlet.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/Servlet.cs new file mode 100644 index 0000000..12b993d --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/Servlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Features.FreeBusy +{ + abstract public class Servlet + { + private string _request; + protected string Url { get; private set; } + protected StreamReader In { get; private set; } + protected StreamWriter Out { get; private set; } + + public void Init(string request, string url, StreamReader reader, StreamWriter writer) + { + this._request = request; + this.Url = url; + this.In = reader; + this.Out = writer; + } + + public void Process() + { + ProcessHeaders(); + ProcessRequest(); + } + + virtual protected void ProcessHeaders() + { + for (;;) + { + string s = In.ReadLine(); + if (string.IsNullOrEmpty(s)) + break; + ProcessHeader(s); + } + } + + virtual protected void ProcessHeader(string s) + { + + } + + abstract protected void ProcessRequest(); + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs index c6092c9..a55c4a3 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs @@ -580,7 +580,6 @@ namespace Acacia.Features.GAB // Update the groups AddItemToGroups(contact, id, value, index); } - } private void CreateGroup(string id, Dictionary value, ChunkIndex index) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/Enums.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/Enums.cs index 4c13a37..deadd05 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/Enums.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/Enums.cs @@ -75,4 +75,13 @@ namespace Acacia.Stubs EAS, Other } + + // Replacement for OlMailRecipientType + public enum MailRecipientType + { + Originator = 0, + To = 1, + CC = 2, + BCC = 3 + } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMailItem.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMailItem.cs index 5f775fe..7f33495 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMailItem.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMailItem.cs @@ -27,9 +27,15 @@ namespace Acacia.Stubs /// public interface IMailItem : IItem { + #region Reply verbs + DateTime? AttrLastVerbExecutionTime { get; set; } int AttrLastVerbExecuted { get; set; } + #endregion + + #region Sender + string SenderEmailAddress { get; } string SenderName { get; } @@ -38,5 +44,16 @@ namespace Acacia.Stubs /// /// The address. The caller is responsible for disposing. void SetSender(IAddressEntry addressEntry); + + #endregion + + #region Recipients + + string To { get; set; } + string CC { get; set; } + string BCC { get; set; } + IRecipients Recipients { get; } + + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipient.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipient.cs index 0127eb9..27ca079 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipient.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipient.cs @@ -24,11 +24,14 @@ namespace Acacia.Stubs { public interface IRecipient : IComWrapper { + bool Resolve(); bool IsResolved { get; } string Name { get; } string Address { get; } + MailRecipientType Type { get; set; } + /// /// Returns the address entry. The caller is responsible for disposing it. /// diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipients.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipients.cs new file mode 100644 index 0000000..b3eeff6 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipients.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Stubs +{ + public interface IRecipients : IComWrapper, IEnumerable + { + int Count { get; } + void Remove(int index); + IRecipient Add(string name); + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs index 64c72c5..ab6daca 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs @@ -347,8 +347,9 @@ namespace Acacia.Stubs.OutlookWrappers NSOutlook.Recipient recipient = com.Add(session.CreateRecipient(name)); if (recipient == null) return null; - recipient.Resolve(); - return Mapping.Wrap(com.Remove(recipient)); + IRecipient wrapped = Mapping.Wrap(com.Remove(recipient)); + wrapped.Resolve(); + return wrapped; } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MailItemWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MailItemWrapper.cs index 9572987..02e126f 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MailItemWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MailItemWrapper.cs @@ -34,6 +34,8 @@ namespace Acacia.Stubs.OutlookWrappers #region IMailItem implementation + #region Reply verbs + public DateTime? AttrLastVerbExecutionTime { get @@ -58,6 +60,10 @@ namespace Acacia.Stubs.OutlookWrappers } } + #endregion + + #region Sender + public string SenderEmailAddress { get @@ -88,6 +94,35 @@ namespace Acacia.Stubs.OutlookWrappers #endregion + #region Recipients + + public string To + { + get { return _item.To; } + set { _item.To = value; } + } + + public string CC + { + get { return _item.CC; } + set { _item.CC = value; } + } + + public string BCC + { + get { return _item.BCC; } + set { _item.BCC = value; } + } + + public IRecipients Recipients + { + get { return new RecipientsWrapper(_item.Recipients); } + } + + #endregion + + #endregion + #region Wrapper methods protected override NSOutlook.UserProperties GetUserProperties() diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientWrapper.cs index b589f76..82749b9 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientWrapper.cs @@ -32,6 +32,17 @@ namespace Acacia.Stubs.OutlookWrappers internal NSOutlook.Recipient RawItem { get { return _item; } } + public MailRecipientType Type + { + get { return (MailRecipientType)_item.Type; } + set { _item.Type = (int)value; } + } + + public bool Resolve() + { + return _item.Resolve(); + } + public bool IsResolved { get diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientsWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientsWrapper.cs new file mode 100644 index 0000000..31d85c2 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/RecipientsWrapper.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NSOutlook = Microsoft.Office.Interop.Outlook; + +namespace Acacia.Stubs.OutlookWrappers +{ + class RecipientsWrapper : ComWrapper, IRecipients + { + public RecipientsWrapper(NSOutlook.Recipients item) : base(item) + { + } + + public int Count { get { return _item.Count; } } + + public void Remove(int index) + { + _item.Remove(index + 1); + } + + public IRecipient Add(string name) + { + return new RecipientWrapper(_item.Add(name)); + } + + public IEnumerator GetEnumerator() + { + foreach (NSOutlook.Recipient recipient in _item) + yield return new RecipientWrapper(recipient); + } + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (NSOutlook.Recipient recipient in _item) + yield return new RecipientWrapper(recipient); + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs index bc60279..c622f44 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs @@ -29,6 +29,7 @@ using System.Globalization; using Acacia.UI.Outlook; using Acacia.Stubs; using Acacia.Stubs.OutlookWrappers; +using Microsoft.Office.Tools.Ribbon; namespace Acacia { @@ -77,7 +78,16 @@ namespace Acacia #region Startup / Shutdown - private void ThisAddIn_Startup(object sender, System.EventArgs args) + private void InternalStartup() + { + // Do nothing + } + + /// + /// [KOE-87] Use this event rather than Startup, as that is invoked after the ribbon UI is queried, which leads + /// to an empty - and therefore invisible - ribbon. + /// + public override void BeginInit() { try { @@ -145,7 +155,6 @@ namespace Acacia Acacia.Features.DebugSupport.Statistics.StartupTime.Stop(); foreach (Feature feature in Features) feature.AfterStartup(); - } catch (System.Exception e) { @@ -171,12 +180,6 @@ namespace Acacia } } - private void ThisAddIn_Shutdown(object sender, System.EventArgs e) - { - // Note: Outlook no longer raises this event. If you have code that - // must run when Outlook shuts down, see http://go.microsoft.com/fwlink/?LinkId=506785 - } - #endregion #region Ribbons @@ -209,19 +212,6 @@ namespace Acacia } #endregion - - #region VSTO generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InternalStartup() - { - this.Startup += new System.EventHandler(ThisAddIn_Startup); - this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown); - } - - #endregion + } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs index 6d990c0..4d4055b 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs @@ -49,6 +49,7 @@ namespace Acacia.Utils return _this; } + #endregion #region Hex strings @@ -112,19 +113,43 @@ namespace Acacia.Utils public static string ReplaceStringTokens(this string s, string open, string close, TokenReplacer replacer) { - return Regex.Replace(s, Regex.Escape(open) + @"(\w+)" + Regex.Escape(close), (m) => + StringBuilder replaced = new StringBuilder(); + + int start = 0; + for(;;) { - var key = m.Groups[1].Value; + // Find open token + int newStart = s.IndexOf(open, start); + + // Not found, append rest and done + if (newStart < 0) + { + replaced.Append(s.Substring(start)); + break; + } + + // Append current text + replaced.Append(s.Substring(start, newStart - start)); + + // Find the close token + int keyStart = newStart + open.Length; + int newClose = s.IndexOf(close, keyStart); + if (newClose < 0) + { + break; + } + + // Add the replacement + string key = s.Substring(keyStart, newClose - keyStart); string replacement = replacer(key); - if (replacement == null) - { - return m.Groups[0].Value; - } - else - { - return replacement; - } - }); + if (replacement != null) + replaced.Append(replacement); + + // Next + start = newClose + close.Length; + } + + return replaced.ToString(); } public static string ReplaceStringTokens(this string s, string open, string close, Dictionary replacements) @@ -138,5 +163,14 @@ namespace Acacia.Utils } #endregion + + + public static string DecodeQuotedPrintable(this string _this) + { + return ReplaceStringTokens(_this, "=?", "?=", (token) => + System.Net.Mail.Attachment.CreateAttachmentFromString("", "=?" + token + "?=").Name + ); + } + } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/FolderRegistration.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/FolderRegistration.cs index d3f6938..ecb9175 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/FolderRegistration.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/FolderRegistration.cs @@ -57,4 +57,30 @@ namespace Acacia.ZPush return Feature.Name + ":" + _itemType.ToString(); } } + + public class FolderRegistrationDefault : FolderRegistration + { + private readonly DefaultFolder _folder; + + public FolderRegistrationDefault(Feature feature, DefaultFolder folder) + : + base(feature) + { + this._folder = folder; + } + + public override bool IsApplicable(IFolder folder) + { + // TODO: cache folder id per store + using (IStore store = folder.GetStore()) + { + return folder.EntryID == store.GetDefaultFolderId(_folder); + } + } + + public override string ToString() + { + return Feature.Name + ":" + _folder.ToString(); + } + } }