1
0
mirror of https://github.com/Kopano-dev/kopano-ol-extension.git synced 2023-10-10 13:37:40 +02:00

[KOE-23] BCC field is now parsed from the transport headers and displayed in sent items

This commit is contained in:
Patrick Simpson 2017-03-15 16:13:15 +01:00
parent 70d9d00ea8
commit df356741d5
13 changed files with 365 additions and 13 deletions

View File

@ -241,6 +241,7 @@
<Compile Include="Controls\KUITask.cs" /> <Compile Include="Controls\KUITask.cs" />
<Compile Include="Controls\KUIUtil.cs" /> <Compile Include="Controls\KUIUtil.cs" />
<Compile Include="DebugOptions.cs" /> <Compile Include="DebugOptions.cs" />
<Compile Include="Features\BCC\FeatureBCC.cs" />
<Compile Include="Features\FreeBusy\FreeBusyServlet.cs" /> <Compile Include="Features\FreeBusy\FreeBusyServlet.cs" />
<Compile Include="Features\FreeBusy\Servlet.cs" /> <Compile Include="Features\FreeBusy\Servlet.cs" />
<Compile Include="Features\SecondaryContacts\FeatureSecondaryContacts.cs" /> <Compile Include="Features\SecondaryContacts\FeatureSecondaryContacts.cs" />
@ -311,6 +312,7 @@
<Compile Include="Stubs\IOutlookWindow.cs" /> <Compile Include="Stubs\IOutlookWindow.cs" />
<Compile Include="Stubs\IPicture.cs" /> <Compile Include="Stubs\IPicture.cs" />
<Compile Include="Stubs\IRecipient.cs" /> <Compile Include="Stubs\IRecipient.cs" />
<Compile Include="Stubs\IRecipients.cs" />
<Compile Include="Stubs\ISignature.cs" /> <Compile Include="Stubs\ISignature.cs" />
<Compile Include="Stubs\ISignatures.cs" /> <Compile Include="Stubs\ISignatures.cs" />
<Compile Include="Stubs\IStores.cs" /> <Compile Include="Stubs\IStores.cs" />
@ -325,6 +327,7 @@
<Compile Include="Stubs\OutlookWrappers\ItemsWrapper.cs" /> <Compile Include="Stubs\OutlookWrappers\ItemsWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\OutlookItemWrapper.cs" /> <Compile Include="Stubs\OutlookWrappers\OutlookItemWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\PictureWrapper.cs" /> <Compile Include="Stubs\OutlookWrappers\PictureWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\RecipientsWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\RecipientWrapper.cs" /> <Compile Include="Stubs\OutlookWrappers\RecipientWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\SignaturesWrapper.cs" /> <Compile Include="Stubs\OutlookWrappers\SignaturesWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\SignatureWrapper.cs" /> <Compile Include="Stubs\OutlookWrappers\SignatureWrapper.cs" />

View File

@ -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<IMailItem>(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
}
}

View File

@ -27,6 +27,7 @@ namespace Acacia.Features
public static readonly Type[] FEATURES = public static readonly Type[] FEATURES =
{ {
typeof(ReplyFlags.FeatureReplyFlags), typeof(ReplyFlags.FeatureReplyFlags),
typeof(BCC.FeatureBCC),
typeof(OutOfOffice.FeatureOutOfOffice), typeof(OutOfOffice.FeatureOutOfOffice),
typeof(SharedFolders.FeatureSharedFolders), typeof(SharedFolders.FeatureSharedFolders),
typeof(WebApp.FeatureWebApp), typeof(WebApp.FeatureWebApp),

View File

@ -75,4 +75,13 @@ namespace Acacia.Stubs
EAS, EAS,
Other Other
} }
// Replacement for OlMailRecipientType
public enum MailRecipientType
{
Originator = 0,
To = 1,
CC = 2,
BCC = 3
}
} }

View File

@ -27,9 +27,15 @@ namespace Acacia.Stubs
/// </summary> /// </summary>
public interface IMailItem : IItem public interface IMailItem : IItem
{ {
#region Reply verbs
DateTime? AttrLastVerbExecutionTime { get; set; } DateTime? AttrLastVerbExecutionTime { get; set; }
int AttrLastVerbExecuted { get; set; } int AttrLastVerbExecuted { get; set; }
#endregion
#region Sender
string SenderEmailAddress { get; } string SenderEmailAddress { get; }
string SenderName { get; } string SenderName { get; }
@ -38,5 +44,16 @@ namespace Acacia.Stubs
/// </summary> /// </summary>
/// <param name="addressEntry">The address. The caller is responsible for disposing.</param> /// <param name="addressEntry">The address. The caller is responsible for disposing.</param>
void SetSender(IAddressEntry addressEntry); void SetSender(IAddressEntry addressEntry);
#endregion
#region Recipients
string To { get; set; }
string CC { get; set; }
string BCC { get; set; }
IRecipients Recipients { get; }
#endregion
} }
} }

View File

@ -24,11 +24,14 @@ namespace Acacia.Stubs
{ {
public interface IRecipient : IComWrapper public interface IRecipient : IComWrapper
{ {
bool Resolve();
bool IsResolved { get; } bool IsResolved { get; }
string Name { get; } string Name { get; }
string Address { get; } string Address { get; }
MailRecipientType Type { get; set; }
/// <summary> /// <summary>
/// Returns the address entry. The caller is responsible for disposing it. /// Returns the address entry. The caller is responsible for disposing it.
/// </summary> /// </summary>

View File

@ -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<IRecipient>
{
int Count { get; }
void Remove(int index);
IRecipient Add(string name);
}
}

View File

@ -347,8 +347,9 @@ namespace Acacia.Stubs.OutlookWrappers
NSOutlook.Recipient recipient = com.Add(session.CreateRecipient(name)); NSOutlook.Recipient recipient = com.Add(session.CreateRecipient(name));
if (recipient == null) if (recipient == null)
return null; return null;
recipient.Resolve(); IRecipient wrapped = Mapping.Wrap(com.Remove(recipient));
return Mapping.Wrap(com.Remove(recipient)); wrapped.Resolve();
return wrapped;
} }
} }

View File

@ -34,6 +34,8 @@ namespace Acacia.Stubs.OutlookWrappers
#region IMailItem implementation #region IMailItem implementation
#region Reply verbs
public DateTime? AttrLastVerbExecutionTime public DateTime? AttrLastVerbExecutionTime
{ {
get get
@ -58,6 +60,10 @@ namespace Acacia.Stubs.OutlookWrappers
} }
} }
#endregion
#region Sender
public string SenderEmailAddress public string SenderEmailAddress
{ {
get get
@ -88,6 +94,35 @@ namespace Acacia.Stubs.OutlookWrappers
#endregion #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 #region Wrapper methods
protected override NSOutlook.UserProperties GetUserProperties() protected override NSOutlook.UserProperties GetUserProperties()

View File

@ -32,6 +32,17 @@ namespace Acacia.Stubs.OutlookWrappers
internal NSOutlook.Recipient RawItem { get { return _item; } } 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 public bool IsResolved
{ {
get get

View File

@ -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<NSOutlook.Recipients>, 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<IRecipient> 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);
}
}
}

View File

@ -49,6 +49,7 @@ namespace Acacia.Utils
return _this; return _this;
} }
#endregion #endregion
#region Hex strings #region Hex strings
@ -112,19 +113,43 @@ namespace Acacia.Utils
public static string ReplaceStringTokens(this string s, string open, string close, TokenReplacer replacer) 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); string replacement = replacer(key);
if (replacement == null) if (replacement != null)
{ replaced.Append(replacement);
return m.Groups[0].Value;
} // Next
else start = newClose + close.Length;
{ }
return replacement;
} return replaced.ToString();
});
} }
public static string ReplaceStringTokens(this string s, string open, string close, Dictionary<string, string> replacements) public static string ReplaceStringTokens(this string s, string open, string close, Dictionary<string, string> replacements)
@ -138,5 +163,14 @@ namespace Acacia.Utils
} }
#endregion #endregion
public static string DecodeQuotedPrintable(this string _this)
{
return ReplaceStringTokens(_this, "=?", "?=", (token) =>
System.Net.Mail.Attachment.CreateAttachmentFromString("", "=?" + token + "?=").Name
);
}
} }
} }

View File

@ -57,4 +57,30 @@ namespace Acacia.ZPush
return Feature.Name + ":" + _itemType.ToString(); 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();
}
}
} }