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:
parent
70d9d00ea8
commit
df356741d5
@ -241,6 +241,7 @@
|
||||
<Compile Include="Controls\KUITask.cs" />
|
||||
<Compile Include="Controls\KUIUtil.cs" />
|
||||
<Compile Include="DebugOptions.cs" />
|
||||
<Compile Include="Features\BCC\FeatureBCC.cs" />
|
||||
<Compile Include="Features\FreeBusy\FreeBusyServlet.cs" />
|
||||
<Compile Include="Features\FreeBusy\Servlet.cs" />
|
||||
<Compile Include="Features\SecondaryContacts\FeatureSecondaryContacts.cs" />
|
||||
@ -311,6 +312,7 @@
|
||||
<Compile Include="Stubs\IOutlookWindow.cs" />
|
||||
<Compile Include="Stubs\IPicture.cs" />
|
||||
<Compile Include="Stubs\IRecipient.cs" />
|
||||
<Compile Include="Stubs\IRecipients.cs" />
|
||||
<Compile Include="Stubs\ISignature.cs" />
|
||||
<Compile Include="Stubs\ISignatures.cs" />
|
||||
<Compile Include="Stubs\IStores.cs" />
|
||||
@ -325,6 +327,7 @@
|
||||
<Compile Include="Stubs\OutlookWrappers\ItemsWrapper.cs" />
|
||||
<Compile Include="Stubs\OutlookWrappers\OutlookItemWrapper.cs" />
|
||||
<Compile Include="Stubs\OutlookWrappers\PictureWrapper.cs" />
|
||||
<Compile Include="Stubs\OutlookWrappers\RecipientsWrapper.cs" />
|
||||
<Compile Include="Stubs\OutlookWrappers\RecipientWrapper.cs" />
|
||||
<Compile Include="Stubs\OutlookWrappers\SignaturesWrapper.cs" />
|
||||
<Compile Include="Stubs\OutlookWrappers\SignatureWrapper.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<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
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -75,4 +75,13 @@ namespace Acacia.Stubs
|
||||
EAS,
|
||||
Other
|
||||
}
|
||||
|
||||
// Replacement for OlMailRecipientType
|
||||
public enum MailRecipientType
|
||||
{
|
||||
Originator = 0,
|
||||
To = 1,
|
||||
CC = 2,
|
||||
BCC = 3
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,15 @@ namespace Acacia.Stubs
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="addressEntry">The address. The caller is responsible for disposing.</param>
|
||||
void SetSender(IAddressEntry addressEntry);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Recipients
|
||||
|
||||
string To { get; set; }
|
||||
string CC { get; set; }
|
||||
string BCC { get; set; }
|
||||
IRecipients Recipients { get; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the address entry. The caller is responsible for disposing it.
|
||||
/// </summary>
|
||||
|
15
src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipients.cs
Normal file
15
src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IRecipients.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string, string> 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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user