diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index 5a2c5e5..b253358 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -241,6 +241,7 @@ + @@ -311,6 +312,7 @@ + @@ -325,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/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/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(); + } + } }