1
0
mirror of https://github.com/Kopano-dev/kopano-ol-extension.git synced 2023-10-10 13:37:40 +02:00
This commit is contained in:
Patrick Simpson 2017-03-20 11:33:44 +01:00
commit effdc32f9e
19 changed files with 529 additions and 116 deletions

View File

@ -241,6 +241,9 @@
<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" />
<Compile Include="Features\DebugSupport\AboutDialog.cs">
<SubType>Form</SubType>
@ -309,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" />
@ -323,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" />

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 =
{
typeof(ReplyFlags.FeatureReplyFlags),
typeof(BCC.FeatureBCC),
typeof(OutOfOffice.FeatureOutOfOffice),
typeof(SharedFolders.FeatureSharedFolders),
typeof(WebApp.FeatureWebApp),

View File

@ -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));
}
}

View File

@ -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<string, ServletFactory> _servlets = new Dictionary<string, ServletFactory>();
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";
}
}
}
}

View File

@ -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";
}
}
}
}

View File

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

View File

@ -580,7 +580,6 @@ namespace Acacia.Features.GAB
// Update the groups
AddItemToGroups(contact, id, value, index);
}
}
private void CreateGroup(string id, Dictionary<string, object> value, ChunkIndex index)

View File

@ -75,4 +75,13 @@ namespace Acacia.Stubs
EAS,
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>
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
}
}

View File

@ -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>

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));
if (recipient == null)
return null;
recipient.Resolve();
return Mapping.Wrap(com.Remove(recipient));
IRecipient wrapped = Mapping.Wrap(com.Remove(recipient));
wrapped.Resolve();
return wrapped;
}
}

View File

@ -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()

View File

@ -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

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

@ -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
}
/// <summary>
/// [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.
/// </summary>
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
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}

View File

@ -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
);
}
}
}

View File

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