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();
+ }
+ }
}