///
/// Copyright © 2003-2008 JetBrains s.r.o.
/// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder).
///
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Diagnostics;
using System.Windows.Forms;
using JetBrains.Omea.Categories;
using JetBrains.Omea.Containers;
using JetBrains.Omea.OpenAPI;
using JetBrains.Omea.Base;
using JetBrains.Omea.ResourceTools;
namespace JetBrains.Omea.Contacts
{
#region IContactManagerProps
internal class ContactManagerProps: IContactManagerProps
{
// links from email to contacts
private readonly int _linkFrom, _linkTo, _linkCC;
// email account properties
private readonly int _linkEmailAcct; // from contacts to accounts
private readonly int _linkEmailAccountFrom; // from emails to accounts
private readonly int _linkEmailAccountTo;
private readonly int _linkEmailAccountCC;
private readonly int _propEmailAddress;
private readonly int _propUserAccount, _propDomain;
private readonly int _propIsPersonalAccount;
private readonly int _propShowOriginalNames;
// Contact Name resource type, links and properties
// Links "NameFrom", "NameTo", "NameCC" link from e-mail (article, ect)
// to the particularly used name. ContactName_s are linked to the
// actual contact by the link "Contact".
private readonly int _linkNameFrom, _linkNameTo, _linkNameCC;
private readonly int _linkBaseContact;
// This link connects a contact and a those resource types which
// represent linked correspondence resources.
private readonly int _linkLinkedResourceTypes;
private readonly int _propLastCorrespondenceDate;
private readonly int _propMySelf;
private readonly int _propIgnored;
private readonly int _propPictureThumbnail;
private readonly int _propPictureOriginal;
internal ContactManagerProps( IResourceStore store )
{
// "From", "To" and "CC" link Email with Contact
_linkFrom = ResourceTypeHelper.UpdatePropTypeRegistration( "From", PropDataType.Link, PropTypeFlags.DirectedLink);
_linkTo = ResourceTypeHelper.UpdatePropTypeRegistration( "To", PropDataType.Link, PropTypeFlags.DirectedLink);
_linkCC = ResourceTypeHelper.UpdatePropTypeRegistration( "CC", PropDataType.Link, PropTypeFlags.DirectedLink);
store.PropTypes.RegisterDisplayName( _linkFrom, "From", "Sent" );
store.PropTypes.RegisterDisplayName( _linkTo, "To", "Received" );
store.PropTypes.RegisterDisplayName( _linkCC, "CC", "Received CC" );
_propIgnored = store.PropTypes.Register( "IsIgnored", PropDataType.Bool, PropTypeFlags.Internal );
_propMySelf = store.PropTypes.Register( "MySelf", PropDataType.Int, PropTypeFlags.Internal );
_propLastCorrespondenceDate = store.PropTypes.Register( "LastCorrespondDate", PropDataType.Date );
store.PropTypes.RegisterDisplayName( _propLastCorrespondenceDate, "Last Correspondence Date" );
_propIsPersonalAccount = store.PropTypes.Register("IsPersonalAccount", PropDataType.Bool, PropTypeFlags.Internal);
_propShowOriginalNames = store.PropTypes.Register( "ShowOriginalNames", PropDataType.Bool, PropTypeFlags.Internal );
_propPictureThumbnail = store.PropTypes.Register( "PictureThumbnail", PropDataType.Blob, PropTypeFlags.Internal );
_propPictureOriginal = store.PropTypes.Register( "ContactOriginalPicture", PropDataType.Blob, PropTypeFlags.Internal );
// "EmailAcct" links Contact or ContactName with EMailAccount
_linkEmailAcct = store.PropTypes.Register( "EmailAcct", PropDataType.Link, PropTypeFlags.ContactAccount );
// "EmailAccountFrom", "EmailAccountTo" and "EmailAccountCC" link Email with EMailAccount
_linkEmailAccountFrom = ResourceTypeHelper.UpdatePropTypeRegistration( "EmailAccountFrom", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
_linkEmailAccountTo = ResourceTypeHelper.UpdatePropTypeRegistration( "EmailAccountTo", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
_linkEmailAccountCC = ResourceTypeHelper.UpdatePropTypeRegistration( "EmailAccountCC", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
_propEmailAddress = store.PropTypes.Register( "EmailAddress", PropDataType.String );
_propUserAccount = store.PropTypes.Register( "UserAccount", PropDataType.String, PropTypeFlags.Internal );
_propDomain = store.PropTypes.Register( "Domain", PropDataType.String, PropTypeFlags.Internal );
store.PropTypes.RegisterDisplayName( _linkEmailAcct, "E-mail Address" );
store.ResourceTypes.Register( "EmailAccount", "Email Account", "EmailAddress", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex );
store.RegisterUniqueRestriction( "EmailAccount", _propEmailAddress );
//-- ContactName, its links and properties -----------------------
_linkNameFrom = store.PropTypes.Register( "NameFrom", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
_linkNameTo = store.PropTypes.Register( "NameTo", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
_linkNameCC = store.PropTypes.Register( "NameCC", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
_linkBaseContact = store.PropTypes.Register( "BaseContact",PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
if( !store.ResourceTypes.Exist( "ContactName" ) )
store.ResourceTypes.Register( "ContactName", "Contact Name", "Name|BaseContact", ResourceTypeFlags.NoIndex | ResourceTypeFlags.Internal );
else
{
store.ResourceTypes[ "ContactName" ].ResourceDisplayNameTemplate = "Name|BaseContact";
store.ResourceTypes[ "ContactName" ].Flags = ResourceTypeFlags.NoIndex | ResourceTypeFlags.Internal;
}
store.RegisterLinkRestriction( "ContactName", _linkBaseContact, "Contact", 1, 1 );
_linkLinkedResourceTypes = store.PropTypes.Register( "LinkedResourcesOfType", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
Core.ResourceBrowser.RegisterLinksGroup( "Addresses", new int[] { LinkFrom, LinkTo, LinkCC }, ListAnchor.First );
}
public int LinkFrom { get { return _linkFrom; } }
public int LinkTo { get { return _linkTo; } }
public int LinkCC { get { return _linkCC; } }
public int LinkEmailAcct { get { return _linkEmailAcct; } }
public int LinkEmailAcctFrom{ get { return _linkEmailAccountFrom; } }
public int LinkEmailAcctTo { get { return _linkEmailAccountTo; } }
public int LinkEmailAcctCC { get { return _linkEmailAccountCC; } }
public int EmailAddress { get { return _propEmailAddress; } }
public int UserName { get { return _propUserAccount; } }
public int Domain { get { return _propDomain; } }
public int PersonalAccount { get { return _propIsPersonalAccount; } }
public int ShowOriginalNames{ get { return _propShowOriginalNames; } }
public int LinkNameFrom { get { return _linkNameFrom; } }
public int LinkNameTo { get { return _linkNameTo; } }
public int LinkNameCC { get { return _linkNameCC; } }
public int LinkBaseContact { get { return _linkBaseContact; } }
public int LinkLinkedOfType { get { return _linkLinkedResourceTypes; } }
public int Ignored { get { return _propIgnored; } }
public int Myself { get { return _propMySelf; } }
public int LastCorrespondenceDate { get { return _propLastCorrespondenceDate; } }
public int Picture { get { return _propPictureThumbnail; } }
public int PictureOriginal { get { return _propPictureOriginal; } }
}
#endregion IContactManagerProps
//-------------------------------------------------------------------------
public class ContactManager: IContactManager
{
private const string cFieldsDelimiter = "; ";
private static IResourceStore RStore;
private static readonly string[] ContactNativeProps =
{
"Title", "FirstName", "MiddleName", "LastName", "Suffix", "Specificator",
"JobTitle", "Address", "Company", "Description", "HomePage", "Birthday",
"MySelf", "LastCorrespondDate", "Imported"
};
private readonly ContactManagerProps _props;
#region PropertiesDeclaration
//contact properties
public static int _propTitle;
public static int _propFirstName;
public static int _propMiddleName;
public static int _propLastName;
public static int _propSuffix;
public static int _propSpecificator;
public static int _propJobTitle;
public static int _propCompany;
public static int _propAddress;
public static int _propHomePage;
public static int _propBirthday;
public static int _propDescription;
//phone properties
public static int _propPhoneName;
public static int _propPhoneNumber;
public static int _propPhone; // link from contact
public static int _propImported;
public static int _propTransferred;
public static int _propDefaultAccount;
///
/// Property controls the origin of the account. It is set in two cases:
/// 1. user created the contact by himself;
/// 2. user has recieved the contact as resource by mail.
/// Setting this property to true allows to delete the corresponding
/// property if it has no linked mails.
///
public static int _propUserCreated;
// relations between merged contact and its descendants
public static int _propSerializationBlob;
public static int _propSerializationBlobLink;
public static int _propResourceAttach;
public static int _linkDeletedToRecycleBin;
private ContactBO _mySelfContact;
private IResourceList _myselfAccounts;
private IResourceList _MySelfTrackingList;
private static bool IsTraceSuppressed;
private static IResource OperationalSplittedContact;
public int PropFrom { get { return Props.LinkFrom; } }
public int PropTo { get { return Props.LinkTo; } }
public int PropCC { get { return Props.LinkCC; } }
public int PropEmailAcct { get { return Props.LinkEmailAcct; } }
public int PropEmailAddress { get { return Props.EmailAddress; } }
private ArrayList _contactMergeFilters = new ArrayList();
#endregion PropertiesDeclaration
#region Ctor and Initialization
public ContactManager( IResourceStore resourceStore )
{
_props = new ContactManagerProps( resourceStore );
_myselfAccounts = resourceStore.EmptyResourceList;
RegisterContactTypes( resourceStore );
}
public void Initialize()
{
RegisterFilterForFormattedResources();
}
public IContactManagerProps Props
{
get { return _props; }
}
#endregion Ctor and Initialization
#region Properties Definition
private static void RegisterContactTypes( IResourceStore resourceStore )
{
RStore = resourceStore;
_propTitle = RStore.PropTypes.Register( "Title", PropDataType.String );
_propFirstName = RStore.PropTypes.Register( "FirstName", PropDataType.String );
_propMiddleName = RStore.PropTypes.Register( "MiddleName", PropDataType.String );
_propLastName = RStore.PropTypes.Register( "LastName", PropDataType.String );
_propSuffix = RStore.PropTypes.Register( "Suffix", PropDataType.String );
_propSpecificator = RStore.PropTypes.Register( "Specificator", PropDataType.String, PropTypeFlags.Internal );
RStore.PropTypes.RegisterDisplayName( _propFirstName, "First Name" );
RStore.PropTypes.RegisterDisplayName( _propMiddleName, "Middle Name" );
RStore.PropTypes.RegisterDisplayName( _propLastName, "Last Name" );
_propJobTitle = RStore.PropTypes.Register( "JobTitle", PropDataType.String, PropTypeFlags.Internal );
_propCompany = RStore.PropTypes.Register( "Company", PropDataType.String, PropTypeFlags.Internal );
_propAddress = RStore.PropTypes.Register( "Address", PropDataType.String, PropTypeFlags.Internal );
_propHomePage = RStore.PropTypes.Register( "HomePage", PropDataType.String, PropTypeFlags.Internal );
_propBirthday = RStore.PropTypes.Register( "Birthday", PropDataType.Date );
_propDescription = RStore.PropTypes.Register( "Description", PropDataType.String, PropTypeFlags.Internal );
_propImported = RStore.PropTypes.Register( "Imported", PropDataType.Int, PropTypeFlags.Internal );
_propUserCreated = RStore.PropTypes.Register( "UserCreated", PropDataType.Bool, PropTypeFlags.Internal );
_propTransferred = RStore.PropTypes.Register( "MailTranferred", PropDataType.Bool, PropTypeFlags.Internal );
_propDefaultAccount = RStore.PropTypes.Register( "DefaultAccountLink", PropDataType.Link, PropTypeFlags.Internal );
RStore.ResourceTypes.Register( "MailingList", "Mailing List", "EmailAcct", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex );
_propPhoneName = RStore.PropTypes.Register( "PhoneName", PropDataType.String, PropTypeFlags.Internal );
_propPhoneNumber = RStore.PropTypes.Register( "PhoneNumber", PropDataType.String, PropTypeFlags.Internal );
_propPhone = RStore.PropTypes.Register( "Phone", PropDataType.Link, PropTypeFlags.Internal );
RStore.ResourceTypes.Register( "Phone", "Phone", "PhoneName PhoneNumber", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex );
ContactBO.PhonesCleanUp();
RStore.ResourceTypes.Register( "ContactSerializationBlobKeeper", "", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex );
_propSerializationBlob = RStore.PropTypes.Register( "SerializationBlob", PropDataType.Blob, PropTypeFlags.Internal );
_propSerializationBlobLink = ResourceTypeHelper.UpdatePropTypeRegistration( "SerializationBlobLink", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal);
_propResourceAttach = RStore.PropTypes.Register( "ResourceAttachment", PropDataType.Link, PropTypeFlags.DirectedLink );
RStore.PropTypes.RegisterDisplayName( _propResourceAttach, "Resource Attachment", "Received with" );
if( !RStore.ResourceTypes.Exist( "Contact" ) )
RStore.ResourceTypes.Register( "Contact", "Title FirstName MiddleName LastName Suffix Specificator| EmailAcct" );
if( RStore.PropTypes.Exist( "SenderName" ))
{
IPropType pt = RStore.PropTypes[ "SenderName" ];
RStore.PropTypes.Delete( pt.Id );
}
IsTraceSuppressed = Core.SettingStore.ReadBool( "Contacts", "SuppressTraces", false );
ResourceTypeHelper.UpdatePropTypeRegistration( "EmailAccountFrom", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
ResourceTypeHelper.UpdatePropTypeRegistration( "EmailAccountTo", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
ResourceTypeHelper.UpdatePropTypeRegistration( "EmailAccountCC", PropDataType.Link, PropTypeFlags.DirectedLink | PropTypeFlags.Internal );
}
private static void RegisterFilterForFormattedResources()
{
IResourceTypeCollection types = Core.ResourceStore.ResourceTypes;
foreach( IResourceType type in types )
{
if( (type.Flags | ResourceTypeFlags.FileFormat) > 0 )
Core.ResourceBrowser.RegisterLinksPaneFilter( type.Name, new ItemRecipientsFilter() );
}
}
#endregion Properties Definition
#region MergeContacts
public IResource Merge( string fullName, IResourceList contacts )
{
#region Preconditions
if( String.IsNullOrEmpty( fullName ))
throw new ArgumentNullException( "fullName", "Full name can not be empty" );
#endregion Preconditions
string title, firstName, midName, lastName, suffix, addSpec;
bool result = ContactResolver.ResolveName( fullName, null, out title,
out firstName, out midName,
out lastName, out suffix, out addSpec );
Debug.Assert( result );
return Merge( title, firstName, midName, lastName, suffix, addSpec, contacts );
}
public IResource Merge( string firstName, string lastName, IResourceList contacts )
{
return Merge( null, firstName, null, lastName, null, null, contacts );
}
public IResource Merge( string title, string firstName, string midName,
string lastName, string suffix, string addSpec,
IResourceList contacts )
{
#region Preconditions
if( String.IsNullOrEmpty( firstName ) && String.IsNullOrEmpty( lastName ))
throw new ArgumentNullException( "firstName", "First name and Last name can not be empty simultaneously" );
if( contacts.Count < 2 )
throw new InvalidOperationException( "Merge is possible with number of contacts >= 2" );
#endregion Preconditions
ContactBO targetContact = CreateContactImpl( title, firstName, midName, lastName, suffix );
if( Core.ProgressWindow != null )
Core.ProgressWindow.UpdateProgress( 0, "Merging contacts...", "" );
targetContact.Resource.BeginUpdate();
int counter = 0;
foreach( IResource res in contacts )
{
ContactBO contact = new ContactBO( res );
//-------------------------------------------------------------
// All links from this contact to its serialized siblings will
// be duplicated to the target contact. So there is no need
// to keep this contact (and its resource) alive. Thus we
// maintain flat structure of serialized contacts.
//-------------------------------------------------------------
if( !contact.IsSerializationContainer )
SerializeContact( contact.Resource, targetContact.Resource );
int condCount = contact.Resource.GetLinksOfType( null, "LinkedSetValue" ).Count;
Trace.WriteLineIf( !IsTraceSuppressed, "ContactManager -- Resource [" + contact.Resource.DisplayName + "] has " + condCount + " links to conditions before Merge" );
MergeData( contact, targetContact );
// Correct the date of the last correspondence
if( contact.LastCorrespondDate > targetContact.LastCorrespondDate )
targetContact.LastCorrespondDate = contact.LastCorrespondDate;
if( contact.IsMyself )
targetContact.SetMyself();
if( Core.ProgressWindow != null )
Core.ProgressWindow.UpdateProgress( (int)(counter * 100.0 / contacts.Count), "Merging contacts...", "" );
}
targetContact.Resource.EndUpdate();
targetContact.QueueIndexing();
// Do not forget to remove these contact after they are
// already saved (serialized)
foreach( IResource res in contacts )
res.Delete();
if( Core.ProgressWindow != null )
Core.ProgressWindow.UpdateProgress( 100, "Merging...", "" );
return targetContact.Resource;
}
private static void SerializeContact( IResource contact, IResource root )
{
Stream strm = ResourceBinarySerialization.Serialize( contact );
IResource blobKeeper = RStore.BeginNewResource( "ContactSerializationBlobKeeper" );
blobKeeper.SetProp( _propSerializationBlob, strm );
blobKeeper.DisplayName = contact.DisplayName;
blobKeeper.EndUpdate();
root.AddLink( _propSerializationBlobLink, blobKeeper );
}
internal void MergeData( ContactBO contact, ContactBO newContact )
{
contact.Resource.BeginUpdate();
newContact.Resource.BeginUpdate();
DateTime lastCorrDate = newContact.LastCorrespondDate;
DateTime dt = contact.LastCorrespondDate;
if( contact.Address.Length > 0 ) // avoid non-necessary assignment
newContact.Address = CatenateString( newContact.Address, contact.Address );
if( contact.Company.Length > 0 ) // avoid non-necessary assignment
newContact.Company = CatenateString( newContact.Company, contact.Company );
if( contact.JobTitle.Length > 0 ) // avoid non-necessary assignment
newContact.JobTitle = CatenateString( newContact.JobTitle, contact.JobTitle );
if( contact.Description.Length > 0 ) // avoid non-necessary assignment
newContact.Description = CatenateString( newContact.Description, contact.Description );
if(( dt > lastCorrDate || newContact.HomePage == string.Empty ) && contact.HomePage != string.Empty )
newContact.HomePage = contact.HomePage;
MergePhones( contact, newContact );
RetargetContactNameLinks( contact.Resource, newContact.Resource );
RelinkExternals( contact.Resource, newContact.Resource );
ReassignPropertiesAndDirectLinks( contact.Resource, newContact.Resource );
contact.Resource.EndUpdate();
newContact.Resource.EndUpdate();
}
// Catenate new string only if it is non empty and not contained already
// in the existing one. Add delimiter only if catenation is necessary.
// NB: assume that string can not be null since they are coming as text
// properties from IContact which returns "" in such cases.
private static string CatenateString( string existing, string newString )
{
if( newString.Length == 0 )
return existing;
// If the content of the string contains our delimiter inside,
// our separation will be errorneous, but we can not forecast
// the presence of any particular delimiter until we use
// StringList property for such string.
string[] fields = Utils.SplitString( existing, cFieldsDelimiter );
foreach( string str in fields )
{
if( str.Trim() == newString )
return existing;
}
string result = existing;
if( result.Length > 0 )
result += cFieldsDelimiter;
result += newString;
return result;
}
private static void MergePhones( IContact contact, ContactBO newContact )
{
string[] phoneNames = contact.GetPhoneNames();
foreach( string name in phoneNames )
{
string oldNumber = contact.GetPhoneNumber( name );
string newNumber = newContact.GetPhoneNumber( name );
// if there is no telephone with such name, or name exists
// but the phone number is empty (such behavior is done inside
// PhoneBlock.cs when analyzing the content of controls in the
// dialog) - then assign new phone; otherwise find suitable
// numeric extension for the new phone and append it.
if( newNumber == string.Empty )
{
newContact.SetPhoneNumber( name, oldNumber );
}
else
if( oldNumber != string.Empty )
{
if( ContactBO.NormalizedPhoneNumber( newNumber ) !=
ContactBO.NormalizedPhoneNumber( oldNumber ) )
{
string newName = ComposeSuitablePhoneName( newContact, name );
newContact.SetPhoneNumber( newName, oldNumber );
}
}
}
}
public static string ComposeSuitablePhoneName( ContactBO contact, string basename )
{
#region Preconditions
if( String.IsNullOrEmpty( basename ))
throw new ArgumentNullException( "basename", "PhoneNameConstruction -- base name for a phone is null" );
#endregion Preconditions
for( int i = 2;; i++ )
{
string pattern = basename + "(" + i + ")";
if( !contact.IsPhoneNameExists( pattern ) )
return pattern;
}
}
//---------------------------------------------------------------------
// Enumerate all properties which are not "native" for the Contact
// class, that is the properties which are set by other parties to the
// contact resource. These properties are to be propagated to the
// target Contact.
// NB: 1. Links from the contact to other resources are also added to
// the target contact.
// 2. Contacts may be linked between each other with undirected
// links or with directed ones (when dest is merged first). This
// leads to cyclic links when merging source and dest contacts.
//---------------------------------------------------------------------
private static void ReassignPropertiesAndDirectLinks( IResource contact, IResource newContact )
{
// Copy all properties from the contact since some of them may
// change during relinking.
ArrayList props = ArrayListPool.Alloc();
try
{
foreach( IResourceProperty prop in contact.Properties )
props.Add( prop );
foreach( IResourceProperty prop in props )
{
// IResource.Properties returns also incoming links and
// marks them with negative Id. Do not process them here.
// For outcoming links - next execution block
if( prop.PropId >= 0 && prop.DataType != PropDataType.Link &&
Array.IndexOf( ContactNativeProps, prop.Name ) == -1 )
{
// First, do not forget to remove property from the
// original contact since for some of them there may
// exist a uniqueness restriction.
contact.DeleteProp( prop.PropId );
newContact.SetProp( prop.PropId, prop.Value );
}
else
if( prop.PropId >= 0 && prop.DataType == PropDataType.Link )
{
IResourceList list = contact.GetLinksOfType( null, prop.PropId );
string name = Core.ResourceStore.PropTypes[ prop.PropId ].Name;
Debug.Assert( list.Count > 0, "Link " + name + " exists but no linked resource is returned" );
// We have to explicitely remove links from the contact (and
// not when we just delete a contact as whole) beforehand,
// because we need to conform some link restrictions, e.g. link
// "TO" from ICQConversation must have only one recipient.
contact.DeleteLinks( prop.PropId );
for( int i = 0; i < list.Count; i++ )
{
// Avoid cyclic links.
if( list[ i ].Id != newContact.Id )
newContact.AddLink( prop.PropId, list[ i ] );
}
}
}
}
finally
{
ArrayListPool.Dispose( props );
}
}
//---------------------------------------------------------------------
// Retarget links from ContactNames to the new contact as separate
// phase. This allows us to simplify other code.
//---------------------------------------------------------------------
private static void RetargetContactNameLinks( IResource contact, IResource newContact )
{
IResourceList linkedContactNames = contact.GetLinksOfType( "ContactName", Core.ContactManager.Props.LinkBaseContact );
foreach( IResource contactName in linkedContactNames )
contactName.SetProp( Core.ContactManager.Props.LinkBaseContact, newContact );
}
//---------------------------------------------------------------------
// Enumerate all links coming TO the contact, and independently of
// their type - duplicate them to the target contact.
// For resource-correspondent links (From, To, CC) - create ContactName
// resources is necessary.
// NB: Avoid links between merged contacts (e.g. custom links).
//---------------------------------------------------------------------
private void RelinkExternals( IResource contact, IResource newContact )
{
// Copy all properties from the contact since some of them may
// change during relinking.
ArrayList props = ArrayListPool.Alloc();
try
{
foreach( IResourceProperty prop in contact.Properties )
props.Add( prop );
foreach( IResourceProperty prop in props )
{
// Negative PropId - incoming links - exactly what we are looking for
IPropType type = RStore.PropTypes[ prop.PropId ];
if( prop.PropId < 0 && type.DataType == PropDataType.Link &&
((type.Flags & PropTypeFlags.DirectedLink) > 0 ) )
{
RetargetLinks( contact, newContact, -prop.PropId );
}
}
}
finally
{
ArrayListPool.Dispose( props );
}
}
private void RetargetLinks( IResource source, IResource target, int linkId )
{
IResourceList list = source.GetLinksOfType( null, linkId );
foreach( IResource res in list )
{
// Avoid cyclic links - links between merged contacts.
if( res.Id != target.Id && !res.HasLink( linkId, target ) )
{
res.BeginUpdate();
if( IsMajorLink( linkId ) && !HasContactNameAlready( res, linkId, target ) )
{
string fullName = new ContactBO( source ).FullName;
int accntLinkId = GetAccountLinkId( linkId );
IResourceList contactAcc = source.GetLinksOfType( "EmailAccount", Props.LinkEmailAcct );
IResourceList emailAccnts = res.GetLinksOfType( "EmailAccount", accntLinkId );
contactAcc = contactAcc.Intersect( emailAccnts, true );
CreateAndLinkContactName( target, res, (contactAcc.Count > 0)? contactAcc[ 0 ] : null, linkId, fullName );
}
res.DeleteLink( linkId, source );
res.AddLink( linkId, target );
res.EndUpdate();
}
}
}
private bool HasContactNameAlready( IResource res, int linkId, IResource target )
{
int nameLinkId = GetNameLinkId( linkId );
IResource cName = res.GetLinkProp( nameLinkId );
if ( cName != null )
{
IResource baseContact = cName.GetLinkProp( Props.LinkBaseContact );
if ( baseContact != null )
{
return baseContact.Id == target.Id;
}
}
return false;
}
///
/// Find all other contacts which are linked to the same e-mail accounts
/// as the given one.
///
/// Source contact, which accounts are taken for
/// list construction
/// List of contact resources
public static IResourceList GetContactsForMerging( IResource contact )
{
IResourceList feasibleContacts = RStore.EmptyResourceList;
int[] typeIDs = contact.GetLinkTypeIds();
foreach ( int typeID in typeIDs )
{
if( RStore.PropTypes[ typeID ].HasFlag( PropTypeFlags.ContactAccount ) )
{
IResourceList accounts = contact.GetLinksOfType( null, typeID );
foreach ( IResource account in accounts )
{
feasibleContacts = feasibleContacts.Union(
account.GetLinksOfType( "Contact", typeID ), true );
}
}
}
return feasibleContacts.Minus( contact.ToResourceList() );
}
public void RegisterContactMergeFilter( IContactMergeFilter filter )
{
_contactMergeFilters.Add( filter );
}
public IContactMergeFilter[] GetContactMergeFilters()
{
return (IContactMergeFilter[]) _contactMergeFilters.ToArray( typeof (IContactMergeFilter) );
}
#endregion MergeContacts
#region SplitContacts
//---------------------------------------------------------------------
// Split the given contact. Extract not all subcontacts but only those
// which are given in the second parameter .
// Comment: Result list of extracted contacts contains also the
// source contact.
//---------------------------------------------------------------------
public IResourceList Split( IResource contact, IResourceList keepersToExtract )
{
#region Preconditions
IResourceList allContactKeepers = contact.GetLinksOfType( "ContactSerializationBlobKeeper", _propSerializationBlobLink );
foreach( IResource res in keepersToExtract )
{
if( allContactKeepers.IndexOf( res.Id ) == -1 )
throw new ArgumentException( "ContactManager -- not all contacts in the input list are the linked subcontacts of the Source Contact" );
}
#endregion Preconditions
// Check whether we extract all subcontacts.
IResourceList extractedContacts;
if( keepersToExtract.Count == allContactKeepers.Count )
extractedContacts = Split( contact );
else
{
IResourceList coveredCategories;
extractedContacts = SplitInternal( contact, keepersToExtract, out coveredCategories );
Core.TextIndexManager.QueryIndexing( contact.Id );
extractedContacts = extractedContacts.Union( contact.ToResourceList(), true );
}
return extractedContacts;
}
//---------------------------------------------------------------------
// Split the given contact. Extract all subcontacts which are linked
// to it.
// Comment: source contact is removed.
//---------------------------------------------------------------------
public IResourceList Split( IResource contact )
{
if( !contact.HasProp( _propSerializationBlobLink ))
throw new ApplicationException( "Contact can not be split - no corresponding references" );
IResourceList coveredCats;
IResourceList allKeepers = contact.GetLinksOfType( "ContactSerializationBlobKeeper", _propSerializationBlobLink );
IResourceList extractedContacts = SplitInternal( contact, allKeepers, out coveredCats );
IResourceList cnames = contact.GetLinksOfType( "ContactName", Props.LinkBaseContact );
Debug.Assert( cnames.Count == 0 );
// get full set of unresolved links to Categories and From/To/CC mails
IResourceList currCats = contact.GetLinksOfType( "Category", ((CategoryManager)Core.CategoryManager).PropCategory );
IResourceList currFrom = contact.GetLinksOfType( null, Props.LinkFrom );
IResourceList currTo = contact.GetLinksOfType( null, Props.LinkTo );
IResourceList currCC = contact.GetLinksOfType( null, Props.LinkCC );
currCats = currCats.Minus( coveredCats );
if( currCats.Count > 0 || currFrom.Count > 0 || currTo.Count > 0 || currCC.Count > 0 )
{
IResource bestContact = FindBestCandidate( extractedContacts );
foreach( IResource res in currCats )
bestContact.AddLink( ((CategoryManager)Core.CategoryManager).PropCategory, res );
foreach( IResource res in currFrom )
res.AddLink( Props.LinkFrom, bestContact );
foreach( IResource res in currTo )
res.AddLink( Props.LinkTo, bestContact );
foreach( IResource res in currCC )
res.AddLink( Props.LinkCC, bestContact );
}
contact.Delete();
return extractedContacts;
}
private IResourceList SplitInternal( IResource contact, IResourceList keepers,
out IResourceList coveredCats )
{
// start recreate serialized contacts
coveredCats = RStore.EmptyResourceList;
IResourceList extractedContacts = RStore.EmptyResourceList;
OperationalSplittedContact = contact;
contact.BeginUpdate();
if( Core.ProgressWindow != null )
Core.ProgressWindow.UpdateProgress( 0, "Splitting...", "" );
for( int i = 0; i < keepers.Count; i++ )
{
Stream strm = keepers[ i ].GetBlobProp( _propSerializationBlob );
IResource oldContact = ResourceBinarySerialization.Deserialize( strm, RemoveLink);
Trace.WriteLineIf( !IsTraceSuppressed, "ContactManager -- Resource [" + oldContact.DisplayName + "] has " +
oldContact.GetLinksOfType( null, "LinkedSetValue" ).Count + " links to conditions after split" );
// NB: Do not explicitely delete links from current contact to
// the category, because if we perform partial subcontact
// extraction, base contact must still keep its Category links
coveredCats = coveredCats.Union( oldContact.GetLinksOfType( "Category", ((CategoryManager)Core.CategoryManager).PropCategory ), true );
extractedContacts = extractedContacts.Union( oldContact.ToResourceList(), true );
double percent = ((double)(i + 1)) / ((double)keepers.Count) * 95.00;
if( Core.ProgressWindow != null )
Core.ProgressWindow.UpdateProgress( (int)percent, "Splitting...", "" );
}
// Prevent UI updates
foreach( IResource res in extractedContacts )
res.BeginUpdate();
// For every mail which was associated with the base contact after
// merge, try to find the appropriate subcontact by its e-mail
// account - if there is a subcontact which is linked to the mail
// via the same account as the base contact - retarget link
// with explicit deletion from the base.
RelinkNewMail( contact, extractedContacts, Props.LinkEmailAcctFrom, Props.LinkFrom );
RelinkNewMail( contact, extractedContacts, Props.LinkEmailAcctTo, Props.LinkTo );
RelinkNewMail( contact, extractedContacts, Props.LinkEmailAcctCC, Props.LinkCC );
SeparateContactAndSubcontacts( contact, keepers );
RemoveUnlinkedAccounts( contact, extractedContacts );
RemoveObsoleteContactNames( contact, extractedContacts );
foreach( IResource res in extractedContacts )
{
Core.TextIndexManager.QueryIndexing( res.Id );
}
// Say End update for everybody.
contact.EndUpdate();
foreach( IResource res in extractedContacts )
res.EndUpdate();
if( Core.ProgressWindow != null )
Core.ProgressWindow.UpdateProgress( 100, "Splitting...", "" );
OperationalSplittedContact = null;
return extractedContacts;
}
// Remove link from base Contact to its subcontact wrappers as if there
// was no one :-).
private static void SeparateContactAndSubcontacts( IResource contact, IResourceList keepers )
{
foreach( IResource res in keepers )
contact.DeleteLink( _propSerializationBlobLink, res );
}
//---------------------------------------------------------------------
// Whether extracted contact have already their own C-CN-eA-E clumps
// (possibly made earlier, before merge) or not (e.g. in the case of
// single email account and contact) we do not need to keep CNames for
// related emails any more:
// 1. Collect all CNames from the base contact (list1)
// 2. Collect all mails linked to extracted contacts (list2)
// 3. Collect all CNames linked to mails from list2 (list3)
// 4. Remove intersection between list1 and list3.
//---------------------------------------------------------------------
private void RemoveObsoleteContactNames( IResource contact, IResourceList extractedContacts )
{
// 1.
IResourceList baseContactCNames = contact.GetLinksOfType( "ContactName", Props.LinkBaseContact );
// 2.
IResourceList newCorrespondence = RStore.EmptyResourceList;
foreach( IResource extrContact in extractedContacts )
newCorrespondence = newCorrespondence.Union( LinkedCorrespondence( extrContact ), true );
// 3.
IResourceList mailLinkedCNames = RStore.EmptyResourceList;
foreach( IResource mail in newCorrespondence )
{
mailLinkedCNames = mailLinkedCNames.Union( mail.GetLinksOfType( "ContactName", Props.LinkNameFrom ), true );
mailLinkedCNames = mailLinkedCNames.Union( mail.GetLinksOfType( "ContactName", Props.LinkNameTo ), true );
mailLinkedCNames = mailLinkedCNames.Union( mail.GetLinksOfType( "ContactName", Props.LinkNameCC ), true );
}
// 4.
IResourceList extraCNames = baseContactCNames.Intersect( mailLinkedCNames, true );
extraCNames.DeleteAll();
}
//---------------------------------------------------------------------
// Up to this point, base (merged) contact is linked to the email
// accounts of extracted contacts. Remove links to those of accounts
// which:
// 1. have links to extracted contacts
// 2. have no correspondence links to base contact.
//---------------------------------------------------------------------
private void RemoveUnlinkedAccounts( IResource contact, IResourceList extracted )
{
IResourceList mails = LinkedCorrespondence( contact );
if( mails.Count == 0 )
{
return;
}
IResourceList selfAccounts = LinkedAccounts( contact ).Intersect( LinkedAccounts( extracted ), true );
foreach( IResource selfAccount in selfAccounts )
{
if( LinkedAccountCorrespondence( selfAccount ).Intersect( mails, true ).Count == 0 )
{
contact.DeleteLink( Props.LinkEmailAcct, selfAccount );
}
}
}
//---------------------------------------------------------------------
// For all primary correspondence links (currently From, To and CC),
// which (potentially) have link restrictions (depending on the type of
// the vis-a-vis resource) - direcly remove them from the merged contact,
// they will be created in just deserialized subcontact). Thus this
// method is called JUST BEFORE the link is created in deserializator.
//
// Same problem is with links between Contact and ContactName resources.
//---------------------------------------------------------------------
private void RemoveLink( IResource contact, IResource linkedRes, int linkId )
{
if( contact.Type != "Contact" )
throw new ArgumentException( "ContactManager -- Illegal type of deserialized resource during Contact split" );
int normLinkId = Math.Abs( linkId );
if( IsMajorLink( normLinkId ))
{
try
{
OperationalSplittedContact.DeleteLink( normLinkId, linkedRes );
}
catch( Exception )
{}
}
else
if( normLinkId == Props.LinkBaseContact )
{
if( linkedRes.Type != "ContactName" )
throw new ArgumentException( "ContactManager -- Illegal type of linked resource from the deserialized one during Contact split - " +
linkedRes.Type + " - when ContactNames is expected" );
// NB: Pay special attention that method understands
// only undirected links and those coming out FROM the
// resource. For links coming IN to the resource, use negative
// value for the link ID.
if( OperationalSplittedContact.HasLink( -normLinkId, linkedRes ))
{
try
{
OperationalSplittedContact.DeleteLink( normLinkId, linkedRes );
}
catch( Exception )
{}
}
}
}
//---------------------------------------------------------------------
private static void RelinkNewMail( IResource baseContact, IResourceList savedContacts,
int idAccountLink, int idContactLink )
{
IResourceList mails = baseContact.GetLinksOfType( null, idContactLink );
foreach( IResource res in mails )
{
IResourceList accounts = res.GetLinksFrom( "EmailAccount", idAccountLink );
foreach( IResource contact in savedContacts )
{
if( contact.GetLinksOfType( "EmailAccount", "EmailAcct" ).Intersect( accounts, true ).Count > 0 )
{
res.DeleteLink( idContactLink, baseContact );
res.AddLink( idContactLink, contact );
break;
}
}
}
}
#endregion SplitContacts
#region FindContact
public IResourceList FindContactList( string title, string firstName, string midName,
string lastName, string suffix )
{
return FindContactListImpl( title, firstName, midName, lastName, suffix, null );
}
public IResourceList FindContactList( string fullName )
{
IResourceList contacts = RStore.EmptyResourceList;
string title, firstName, midName, lastName, suffix, addSpec;
if( ContactResolver.ResolveName( fullName, null, out title, out firstName, out midName, out lastName, out suffix, out addSpec ) )
contacts = FindContactListImpl( title, firstName, midName, lastName, suffix, null );
return contacts;
}
public IContact FindContact( string fullName )
{
IContact contact = null;
string title, firstName, midName, lastName, suffix, addSpec;
if( ContactResolver.ResolveName( fullName, null, out title, out firstName, out midName, out lastName, out suffix, out addSpec ) )
contact = FindContact( title, firstName, midName, lastName, suffix );
return contact;
}
public IContact FindContact( string title, string firstName, string midName,
string lastName, string suffix )
{
ContactBO contact = null;
IResourceList result = FindContactListImpl( title, firstName, midName, lastName, suffix, null );
//-----------------------------------------------------------------
// It is possible to have several result contacts when no additional
// field is specified, and several contacts match the basic params.
// Choose one which have lesser amount of specificators.
//-----------------------------------------------------------------
if( result.Count == 1 )
contact = new ContactBO( result[ 0 ] );
else
if( result.Count > 1 )
contact = FindRestrictedContact( result );
return contact;
}
//---------------------------------------------------------------------
// Method tries several alternatives to find the contact given its
// name fields and an account:
// 1. Try to find contact only by names. In the case of several contacts
// having been found, select the one which has the given account.
// If no such - select the contact which has lesser number of
// additional restrictions in the name fields.
// 2. If no contact is found by name, try to analyze sender names of
// contacts linked to the account.
//---------------------------------------------------------------------
private IContact FindContact( IResource account, string title, string firstName,
string midName, string lastName, string suffix )
{
ContactBO contact = null;
IResourceList result = FindContactListImpl( title, firstName, midName, lastName, suffix, null );
//-----------------------------------------------------------------
// It is possible to have several result contacts when no additional
// field is specified, and several contacts match the basic params.
// First, perform disambiguation based on account parameter - choose
// those which have such account.
// Then, choose one which have lesser amount of specificators.
//-----------------------------------------------------------------
if( result.Count == 1 )
contact = new ContactBO( result[ 0 ] );
else
{
IResourceList linkedContacts = RStore.EmptyResourceList;
if( account != null )
linkedContacts = account.GetLinksOfType( "Contact", Props.LinkEmailAcct );
if( result.Count > 1 )
{
linkedContacts = linkedContacts.Intersect( result, true );
// If one - return it. If more - use it for next restrictions,
// If no - use original list.
if( linkedContacts.Count == 1 )
contact = new ContactBO( linkedContacts[ 0 ] );
else
{
if( linkedContacts.Count > 1 )
result = linkedContacts;
contact = FindRestrictedContact( result );
}
}
else // (result.Count == 0)
{
string fullName = Utils.MergeStrings( new[]{ title, firstName, midName, lastName, suffix }, ' ' );
// Perform search within ContactNames of contacts linked
// to the specified account;
foreach( IResource res in linkedContacts )
{
if( ContactHasCName( res, fullName ))
{
contact = new ContactBO( res );
break;
}
}
}
}
return contact;
}
private bool ContactHasCName( IResource contact, string fullName )
{
bool isEmpty = fullName.Length == 0;
IResourceList siblings = contact.GetLinksOfType( null, Props.LinkBaseContact );
foreach( IResource cName in siblings.ValidResources )
{
string name = cName.GetStringProp( Core.Props.Name );
if( name == null )
{
if ( isEmpty ) continue;
name = string.Empty;
}
// use the fact that fullname can not be NULL
if( string.Compare( name, fullName, true ) == 0 )
{
return true;
}
}
return false;
}
private static IResourceList FindContactListImpl( string title, string fn, string mn,
string ln, string suff, string addSpec )
{
// Require that anything from the required fields is present.
if( string.IsNullOrEmpty( fn ) && string.IsNullOrEmpty( ln ) )
return RStore.EmptyResourceList;
if( fn == null ) fn = string.Empty;
if( ln == null ) ln = string.Empty;
// No difference, what to compare first.
int propF = _propLastName, propS = _propFirstName;
string valF = ln;
string valS = fn;
if( string.IsNullOrEmpty( ln ))
{
valF = fn; propF = _propFirstName; valS = null;
}
//-----------------------------------------------------------------
IResourceList temp = RStore.FindResources( "Contact", propF, valF );
IResourceList result = RStore.EmptyResourceList;
List contactIds = new List();
bool isValidSecond = !string.IsNullOrEmpty( valS );
foreach( IResource res in temp )
{
string prop = res.GetStringProp( propS );
bool isValidProp = !string.IsNullOrEmpty( prop );
if( ( isValidSecond && isValidProp && string.Compare( prop, valS, true ) == 0 ) ||
( !isValidSecond && !isValidProp ) )
{
contactIds.Add( res.Id );
}
}
if( contactIds.Count > 0 )
{
result = RStore.ListFromIds( contactIds, false );
}
//-----------------------------------------------------------------
// To this point, basic set is constructed. If additional (optional)
// fields are give, restrict this set further.
//-----------------------------------------------------------------
if( !string.IsNullOrEmpty( mn ))
result = IntersectSets( result, mn, _propMiddleName );
if( !string.IsNullOrEmpty( title ))
result = IntersectSets( result, title, _propTitle );
if( !string.IsNullOrEmpty( suff ))
result = IntersectSets( result, suff, _propSuffix );
//-----------------------------------------------------------------
// Do not use constraining on additional specifier conforming
// our policy of its construction.
//if( Utils.IsValidString( addSpec ))
// result = IntersectSets( result, addSpec, _propSpecificator );
//-----------------------------------------------------------------
//-----------------------------------------------------------------
// Among several alternatives, return only those contacts which
// are not "deleted" contacts (not visible in the "Deleted Resources"
// view). Return the list as is if all contact in the list are
// deleted contacts.
//-----------------------------------------------------------------
if( result.Count > 1 )
{
contactIds = new List();
foreach( IResource res in result )
{
if( !res.HasProp( Core.Props.IsDeleted ) )
{
contactIds.Add( res.Id );
}
}
temp = RStore.ListFromIds( contactIds, false );
if( temp.Count > 0 && temp.Count != result.Count )
{
result = temp;
}
}
return result;
}
private static IResourceList IntersectSets( IResourceList orig, string pattern, int propId )
{
return orig.Intersect( Core.ResourceStore.FindResources( null, propId, pattern ), true );
}
private static ContactBO FindRestrictedContact( IResourceList list )
{
int constraintsCounter = int.MaxValue;
IResource resultContact = null;
foreach( IResource res in list )
{
int localCounter = CountConstrainingProps( res );
if( localCounter < constraintsCounter )
{
resultContact = res;
constraintsCounter = localCounter;
}
}
return new ContactBO( resultContact );
}
private static int CountConstrainingProps( IResource res )
{
int counter = 0;
if( res.HasProp( _propMiddleName )) counter++;
if( res.HasProp( _propTitle )) counter++;
if( res.HasProp( _propSuffix )) counter++;
if( res.HasProp( _propSpecificator )) counter++;
return counter;
}
#endregion FindContact
#region FindOrCreateContact
public IContact FindOrCreateContact( string email, string firstName, string lastName )
{
return FindOrCreateContact( email, string.Empty, firstName, string.Empty, lastName, string.Empty );
}
public IContact FindOrCreateContact( string email, string title, string fn,
string mn, string ln, string suffix )
{
#region Preconditions
if( String.IsNullOrEmpty( email ) && String.IsNullOrEmpty( fn ) && String.IsNullOrEmpty( ln ))
throw new ArgumentNullException( "email", "ContactManager -- Email, First Name and Last Name can not be null or empty simultaneously" );
#endregion Preconditions
IContact contact;
IResource account = FindOrCreateEmailAccount( email );
// First, check whether the given account belongs to the set of accounts
// from Myself contact - otherwise we will illegally create extra contact.
if( _mySelfContact != null )
{
if( _myselfAccounts == Core.ResourceStore.EmptyResourceList )
{
_myselfAccounts = _mySelfContact.Resource.GetLinksOfTypeLive( "EmailAccount", Props.LinkEmailAcct );
}
}
if(( account != null ) && ( _myselfAccounts.IndexOf( account.Id ) != -1 ) &&
account.HasProp( Props.PersonalAccount ) )
{
contact = FindOrCreateMySelfContact( email, title, fn, mn, ln, suffix );
}
else
{
contact = FindOrCreateContactImpl( account, title, fn, mn, ln, suffix );
contact.QueueIndexing();
}
return contact;
}
private IContact FindOrCreateContactImpl( IResource account, string title, string fn,
string mn, string ln, string suffix )
{
IContact contact = FindContact( account, title, fn, mn, ln, suffix );
if( contact == null )
{
contact = CreateOrUpdateContact( account, title, fn, mn, ln, suffix );
}
contact.AddAccount( account );
return contact;
}
public IContact FindOrCreateContact( string email, string name )
{
#region Preconditions
if( String.IsNullOrEmpty( email ) && String.IsNullOrEmpty( name ) )
throw new ArgumentNullException( "email", "ContactManager -- Email and SenderName can not be null or empty simultaneously" );
#endregion Preconditions
IContact contact;
string title, fn, mn, ln, suffix, addSpec;
if( ContactResolver.ResolveName( name, email, out title, out fn, out mn, out ln, out suffix, out addSpec ) )
{
contact = FindOrCreateContact( email, title, fn, mn, ln, suffix );
}
else
{
IResource emailAccount = FindOrCreateEmailAccount( email );
if( emailAccount == null )
throw new ArgumentNullException( "emailAccount", "ContactManager - Internal error - Email Account and Unresolved Contact resource can not be null simultaneously" );
// Contact has not been found or created. That means that senderName is
// not valid, while the email account is. Find any existing contact from
// this account or (if there is no one) create new empty one.
IResourceList linkedContacts = emailAccount.GetLinksOfType( "Contact", Props.LinkEmailAcct );
if( linkedContacts.Count > 0 )
{
IResource bestFit = FindBestCandidate( linkedContacts );
contact = new ContactBO( bestFit );
}
else
contact = CreateBlankContact();
contact.AddAccount( emailAccount );
Core.TextIndexManager.QueryIndexing( contact.Resource.Id );
}
return contact;
}
//---------------------------------------------------------------------
// Method has some intricasy in its logic. First of all, it is called
// outside when the caller determines that a given email account
// belongs to the application owner - say MySelf contact. Independently
// of that information, we split all email accounts into two groups:
// - personal: accounts that are truly belong to a contact and are not
// shared wrt any emailing service.
// - group: shared accounts.
// When we meet shared account, we must apply ordinary logic - not
// specific for a myself contact, that is link account to a contact only
// if a sender's name coinsides with the contacts' one.
//---------------------------------------------------------------------
public IContact FindOrCreateMySelfContact( string email, string title, string fn,
string mn, string ln, string suffix )
{
#region Preconditions
if( RStore.FindResources( "Contact", "MySelf", 1 ).Count > 1 )
throw new InvalidProgramException( "ContactManager -- Amount of MYSELF contacts exceeds 1" );
#endregion Preconditions
if( title == null ) title = string.Empty;
if( fn == null ) fn = string.Empty;
if( mn == null ) mn = string.Empty;
if( ln == null ) ln = string.Empty;
if( suffix == null )suffix = string.Empty;
//-----------------------------------------------------------------
ContactBO contact = null;
IResource account = FindOrCreateEmailAccount( email );
if( account == null || account.HasProp( Props.PersonalAccount ) )
{
IResource mySelfRes = RStore.FindUniqueResource( "Contact", "MySelf", 1 );
if ( mySelfRes != null )
{
contact = new ContactBO( mySelfRes );
contact.AddAccount( account );
}
else
{
contact = (ContactBO) FindOrCreateContact( email, title, fn, mn, ln, suffix );
contact.SetMyself();
}
if( _mySelfContact == null )
_mySelfContact = contact;
}
else
{
contact = (ContactBO) FindOrCreateContactImpl( account, title, fn, mn, ln, suffix );
}
contact.QueueIndexing();
return contact;
}
public IContact FindOrCreateMySelfContact( string email, string name )
{
#region Preconditions
if( RStore.FindResources( "Contact", "MySelf", 1 ).Count > 1 )
throw new InvalidProgramException( "ContactManager -- Amount of MYSELF contacts exceeds 1" );
if( String.IsNullOrEmpty( email ) && String.IsNullOrEmpty( name ) )
throw new ArgumentNullException( "email", "ContactManager -- Email and SenderName for Myself can not be null or empty simultaneously." );
#endregion Preconditions
string title, fn, mn, ln, suffix, addSpec;
bool resolved = ContactResolver.ResolveName( name, email, out title, out fn,
out mn, out ln, out suffix, out addSpec );
ContactBO contact = null;
if ( resolved )
contact = (ContactBO) FindOrCreateMySelfContact( email, title, fn, mn, ln, suffix );
else
{
IResource mySelf = RStore.FindUniqueResource( "Contact", "MySelf", 1 );
if ( mySelf != null )
{
contact = new ContactBO( mySelf );
contact.AddAccount( email );
}
else
{
contact = (ContactBO) FindOrCreateContact( email, name );
contact.SetMyself();
}
}
return contact;
}
public IResource FindOrCreateEmailAccount( string email )
{
IResource emailAccount = null;
if( !string.IsNullOrEmpty( email ) )
{
emailAccount = RStore.FindUniqueResource( "EmailAccount", Props.EmailAddress, email );
if ( emailAccount == null )
{
emailAccount = RStore.NewResource( "EmailAccount" );
emailAccount.SetProp( Props.EmailAddress, email );
}
}
return emailAccount;
}
public IResource FindOrCreateMailingList( string email )
{
IResource emailAccount = FindOrCreateEmailAccount( email );
if ( emailAccount == null )
return null;
IResourceList mailLists = emailAccount.GetLinksOfType( "MailingList", Props.LinkEmailAcct );
if ( mailLists.Count > 0 )
return mailLists [ 0 ];
IResource mailList = RStore.BeginNewResource( "MailingList" );
mailList.AddLink( Props.LinkEmailAcct, emailAccount );
mailList.EndUpdate();
return mailList;
}
#endregion FindOrCreateContact
#region CreateContact
public IContact CreateContact( string title, string fName, string midName, string lName, string suffix )
{
return CreateContactImpl( title, fName, midName, lName, suffix );
}
public IContact CreateContact( string fullName )
{
IContact contact = null;
string title, fName, midName, lName, suffix, addSpec;
if( ContactResolver.ResolveName( fullName, null, out title, out fName, out midName, out lName, out suffix, out addSpec ) )
contact = CreateContactImpl( title, fName, midName, lName, suffix );
return contact;
}
public IContact CreateOrUpdateContact( IResource emailAcc, string fullName )
{
string title, fName, midName, lName, suffix, addSpec;
ContactResolver.ResolveName( fullName, emailAcc.DisplayName, out title, out fName, out midName, out lName, out suffix, out addSpec );
// independently of resolve status, either update empty contact from
// the account or create new (empty if not resolved).
return CreateOrUpdateContact( emailAcc, title, fName, midName, lName, suffix );
}
public IContact CreateOrUpdateContact( IResource emailAcc, string title, string firstName,
string midName, string lastName, string suffix )
{
ContactBO contact = FindEmptyContactFromAccount( emailAcc );
if( contact != null )
{
Trace.WriteLine( "ContactManager -- Found empty contact, updating fields in CreateOrUpdateContact." );
contact.Title = title;
contact.FirstName = firstName;
contact.MiddleName = midName;
contact.LastName = lastName;
contact.Suffix = suffix;
}
else
contact = CreateContactImpl( title, firstName, midName, lastName, suffix );
return contact;
}
private static ContactBO CreateBlankContact()
{
return CreateContactImpl( null, null, null, null, null );
}
private static ContactBO CreateContactImpl( string title, string firstName, string midName,
string lastName, string suffix )
{
ContactBO contact = new ContactBO( RStore.NewResource( "Contact" ) );
contact.Title = title;
contact.FirstName = firstName;
contact.MiddleName = midName;
contact.LastName = lastName;
contact.Suffix = suffix;
// contact.AdditionalSpec = addSpec;
return contact;
}
#endregion CreateContact
#region EmptyContacts
private ContactBO FindEmptyContactFromAccount( IResource account )
{
if( account != null )
{
IResourceList contacts = FilterNonBlankContacts(
account.GetLinksOfType( "Contact", Props.LinkEmailAcct ) );
foreach( IResource res in contacts.ValidResources )
{
return new ContactBO( res );
}
}
return null;
}
public static bool IsEmptyContact( IResource contact )
{
return( contact.GetPropText( _propFirstName ).Length == 0 &&
contact.GetPropText( _propLastName ).Length == 0 );
}
public static IResourceList FilterNonBlankContacts( IResourceList contacts )
{
return contacts.Minus( Core.ResourceStore.FindResourcesWithProp( null, _propFirstName ) ).Minus(
Core.ResourceStore.FindResourcesWithProp( null, _propLastName ) );
}
public static bool IsEmptyContact( IContact contact )
{
return( contact.FirstName.Length == 0 && contact.LastName.Length == 0 );
}
internal void DeleteBlankContacts( IResource emailAccount )
{
foreach( IResource res in FilterNonBlankContacts(
emailAccount.GetLinksOfType( "Contact", Props.LinkEmailAcct ) ).ValidResources )
{
DeleteContactImplLight( res );
}
}
#endregion EmptyContacts
#region Delete Contact
public void DeleteContact( IResource contact, bool ignoreContact, out string message )
{
#region Preconditions
if( contact == null )
throw new ArgumentNullException( "contact", "ContactManager -- input contact can not be null" );
if( contact.Type != "Contact" )
throw new ArgumentException( "ContactManager -- Invalid type of input resource in [DeleteContact] - " + contact.Type );
#endregion Preconditions
message = null;
if( !contact.HasProp( Props.Myself )) // Never do anything with Myself !!!
{
if( !contact.HasProp( Core.Props.IsDeleted ) )
{
message = DeleteLinkedCorrespondence( contact );
if( message == null )
{
if( ignoreContact )
contact.SetProp( Props.Ignored, true );
contact.SetProp( Core.Props.IsDeleted, true );
}
}
}
else
{
message = "Omea user information resource can not be deleted";
}
}
public static void RemoveContactFromAddressBook( IResource contact, IResource addressBook )
{
#region Preconditions
if( contact == null )
throw new ArgumentNullException( "contact", "ContactManager -- input contact can not be null" );
if( contact.Type != "Contact" )
throw new ArgumentException( "ContactManager -- Invalid type of input contact resource in [RemoveContactFromAddressBook] - " + contact.Type );
if( addressBook == null )
throw new ArgumentNullException( "addressBook", "ContactManager -- input address book can not be null" );
if( addressBook.Type != "AddressBook" )
throw new ArgumentException( "ContactManager -- Invalid type of input address book resource in [RemoveContactFromAddressBook] - " + addressBook.Type );
#endregion Preconditions
new AddressBook( addressBook ).RemoveContact( contact );
}
//---------------------------------------------------------------------
// Currently the main goal of this method is to clean ContactName
// resources linked to the given one.
//
// If a given resource is the only linked to the ContactName,
// then this ContactName must be deleted as well.
//---------------------------------------------------------------------
public void UnlinkContactInformation( IResource res )
{
#region Preconditions
if( res == null )
throw new ArgumentNullException( "res", "ContactManager -- Input resource can not be NULL" );
#endregion Preconditions
IResourceList linkedCNames = LinkedNameCorrespondence( res );
for( int i = 0; i < linkedCNames.Count; i++ )
{
IResourceList linkedRes = LinkedNameCorrespondence( linkedCNames[ i ] );
if( linkedRes.Count == 1 )
new ResourceProxy( linkedCNames[ i ] ).Delete();
}
}
#region Hanged and Unused Contacts
public void DeleteUnusedContacts( IResourceList contacts )
{
int count = contacts.Count;
for ( int i = count - 1; i >= 0 ; i-- )
{
IResource contact;
try
{
contact = contacts[ i ];
}
catch( StorageException )
{
continue;
}
DeleteHangedContactNames( contact );
DeleteHangedContact( contact );
}
}
private static void DeleteHangedContactNames( IResource contact )
{
IResourceList cNames = contact.GetLinksOfType( "ContactName", Core.ContactManager.Props.LinkBaseContact );
for( int i = 0; i < cNames.Count; i++ )
{
if ( !cNames [i].HasProp( -Core.ContactManager.Props.LinkNameFrom ) &&
!cNames [i].HasProp( -Core.ContactManager.Props.LinkNameTo ) &&
!cNames [i].HasProp( -Core.ContactManager.Props.LinkNameCC ) )
{
new ResourceProxy( cNames[ i ] ).Delete();
}
}
}
//---------------------------------------------------------------------
// Remove contact if it does not associated with any mail, article,
// etc, thus it has links only to email accounts and Contact names.
//---------------------------------------------------------------------
private void DeleteHangedContact( IResource contact )
{
if( !contact.HasProp( Props.Myself ) && !contact.HasProp( _propImported ) &&
!contact.HasProp( ContactManager._propUserCreated ) )
{
int[] linkTypeIDs = contact.GetLinkTypeIds();
foreach ( int linkTypeID in linkTypeIDs )
{
if( !ResourceTypeHelper.IsAccountLink( linkTypeID ) && ( linkTypeID != Core.ContactManager.Props.LinkBaseContact ))
return;
}
string errMsg;
DeleteContact( contact, false, out errMsg );
}
}
#endregion Hanged and Unused Contacts
#region Removal Implementation
public static string DeleteLinkedCorrespondence( IResource contact )
{
bool isSuccess = true;
string errMsg = null;
// To remove contact - first, analyze all linked correspondence:
// - for each res type in list there must be defined an its own
// deleter class;
// - actual removal can be successful.
IResourceList linked = LinkedCorrespondence( contact );
linked = ResourceTypeHelper.ExcludeUnloadedPluginResources( linked );
foreach( IResource res in linked )
{
IResourceDeleter deleter = Core.PluginLoader.GetResourceDeleter( res.Type );
isSuccess = isSuccess && ( deleter != null) && deleter.CanDeleteResource( res, false );
}
Trace.WriteLine( "ContactManager -- Analyzed " + linked.Count + " linked correspondence messages." );
if( isSuccess )
{
// GOOD! all resources (their types have corresponding deleters!!!)
Trace.WriteLine( "ContactManager -- All messages have Deleter support." );
foreach( IResource res in linked )
{
// When performing non-permanent deletion of the linked resources,
// do not call delete of the already deleted items - otherwise they
// potentially will be removed permanently.
if( !res.HasProp( Core.Props.IsDeleted ) )
Core.PluginLoader.GetResourceDeleter( res.Type ).DeleteResource( res );
}
}
else
{
Trace.WriteLine( "ContactManager -- NOT All messages have Deleter support." );
errMsg = "Contact \"" + contact.DisplayName + "\" can not be deleted because of problems with the removal of its correspondence.";
}
return errMsg;
}
//---------------------------------------------------------------------
// When removing a contact:
// 1. Remove linked accounts (only those which linked only to this contact)
// 2. Remove linked contact names
// 3. Remove a contact resource per se.
//---------------------------------------------------------------------
public static void DeleteContactImpl( IResource contact )
{
try
{
int[] linkTypeIDs = contact.GetLinkTypeIds();
foreach ( int linkTypeID in linkTypeIDs )
{
if( ResourceTypeHelper.IsAccountLink( linkTypeID ) )
{
IResourceList contactAccounts = contact.GetLinksOfType( null, linkTypeID );
foreach ( IResource contactAccount in contactAccounts )
{
if( contactAccount.GetLinksOfType( "Contact", linkTypeID ).Count == 1 )
contactAccount.Delete();
}
}
}
DeleteContactImplLight( contact );
}
catch( Exception e )
{
Core.ReportBackgroundException( e );
}
}
private static void DeleteContactImplLight( IResource contact )
{
IResourceList contactNames = contact.GetLinksOfType( "ContactName", Core.ContactManager.Props.LinkBaseContact );
contactNames.DeleteAll();
contact.Delete();
}
#endregion Removal Implementation
#endregion Delete Contact
#region Delete Unnecessary ContactNames
///
/// Delete all resources of type ContactName which are identical to the contact's name -
/// or almost identical (e.g. to the extent of apostrophes).
/// Additionally check several violations - single contact names which are NOT named
/// equally to the contact and those resources which linked by the "BaseContact" link
/// but do not belong to the "ContactName" resource type.
///
/// # of contact names to be deleted and # of illegally named single contact names.
public static void UnlinkIdenticalContactNames( IResourceList contacts, IProgressWindow wnd,
ref int count, ref int illegallyNamedCount )
{
count = illegallyNamedCount = 0;
for( int i = 0; i < contacts.Count; i++ )
{
IResourceList cNames = contacts[ i ].GetLinksOfType( "ContactName", Core.ContactManager.Props.LinkBaseContact );
foreach( IResource cName in cNames )
{
String cname = cName.GetStringProp( Core.Props.Name );
String contact = contacts[ i ].DisplayName;
if( cname == contact || CleanedName( cname ) == contact )
{
new ResourceProxy( cName ).Delete();
count++;
}
else
{
illegallyNamedCount++;
}
}
if( wnd != null && i % 100 == 0 )
{
int percent = i * 100 / contacts.Count;
wnd.UpdateProgress( percent, "Upgrading Contact Names information: " + percent + "%, " + count + " names removed", null );
}
}
}
#endregion Delete Unnecessary ContactNames
#region Contact Linkage
public void LinkContactToResource( int linkId, IResource contact, IResource mail,
string account, string senderName )
{
IResource accRes = FindOrCreateEmailAccount( account );
LinkContactToResource( linkId, contact, mail, accRes, senderName );
}
///
/// Performs basic operations on linking between mail (news article, etc.)
/// resource, contact and emailAccount resources:
/// 1. bind a mail with a contact via "From", "To" or "CC" link;
/// 2. create a new object "ContactName" and bind a mail and CN with "NameFrom",
/// "NameTo" or "NameCC" link, and finally link CN and its base contact
/// object;
/// 3. link email (news article, etc) with emailAccount resource.
///
/// Id of link between a mail and a contact
/// Base contact
/// A resource to be linked (mail, news article etc.)
/// Mail account of a mail.
/// Name which will be shown to the user.
public void LinkContactToResource( int linkId, IResource contact, IResource mail,
IResource account, string senderName )
{
#region Preconditions
if( linkId != Props.LinkFrom && linkId != Props.LinkTo && linkId != Props.LinkCC )
throw new ArgumentException( "ContactManager -- Illegal [propID] property value - not From, To or CC" );
if( contact == null )
throw new ArgumentNullException( "contact", "ContactManager -- Contact resource is null" );
if( mail == null )
throw new ArgumentNullException( "mail", "ContactManager -- Mail/Article resource is null" );
if( contact.Type != "Contact" && contact.Type != "MailingList" )
throw new ArgumentException( "ContactManager -- Illegal resource type - not a Contact" );
// account is allowed to be null or senderName is allowed to be
// null (e.g. in the case of news), but not both simultaneously
if( String.IsNullOrEmpty( senderName ) && (account == null))
throw new ArgumentException( "ContactManager -- Account and Sender name are not allowed to be null simultaneously");
#endregion Preconditions
//-----------------------------------------------------------------
// Link Contact to a Resource Type resource of the mail.
//-----------------------------------------------------------------
IResource type = Core.ResourceStore.FindUniqueResource( "ResourceType", Core.Props.Name, mail.Type );
contact.AddLink( Props.LinkLinkedOfType, type );
//-----------------------------------------------------------------
// First check whether the link is unique, that is only one such
// link can exist between a resource and the contact, e.g. link
// FROM. In such case remove all resources linked by the propId
// and GetNameLinkId( propId ) links.
//-----------------------------------------------------------------
if( IsUniqueLink( linkId, mail.Type ) )
{
mail.SetProp( linkId, contact );
mail.DeleteLinks( GetNameLinkId( linkId ));
}
else
mail.AddLink( linkId, contact );
// Do not link contact name with "MailingList" resources.
if( contact.Type == "Contact" )
CreateAndLinkContactName( contact, mail, account, linkId, senderName );
if( account != null )
{
int linkAccntId = GetAccountLinkId( linkId );
mail.AddLink( linkAccntId, account );
}
//-----------------------------------------------------------------
// In the case when the contact is deleted non-permanently,
// two continuations are possible:
// 1. If a contact is marked as Ignored then all newly incoming
// correspondence from this contact is to be moved to the
// RecycleBin automatically.
// 2. If a contact is NOT marked as Ignored then it is recovered
// from the RecycleBin and appears as ordinary contact.
// Deleting of "IsIgnored" property makes that.
//-----------------------------------------------------------------
if( contact.HasProp( Core.Props.IsDeleted ))
{
if( contact.HasProp( Props.Ignored ) )
{
Core.ResourceAP.QueueJobAt( DateTime.Now.AddMilliseconds( 100.0 ),
new ResourceDelegate( DeleteResourceToRecycleBin ), mail );
}
else
{
contact.DeleteProp( Core.Props.IsDeleted );
}
}
}
private static void DeleteResourceToRecycleBin( IResource res )
{
IResourceDeleter deleter = Core.PluginLoader.GetResourceDeleter( res.Type );
if( !res.HasProp( Core.Props.IsDeleted ) && deleter != null )
deleter.DeleteResource( res );
}
private void CreateAndLinkContactName( IResource contact, IResource mail, IResource accnt,
int linkId, string senderName )
{
#region Preconditions
if( contact.Type != "Contact" )
throw new ArgumentException( "ContactManager -- contract violation: expected type [Contact]." );
#endregion Preconditions
if( string.IsNullOrEmpty( senderName ))
return;
//-----------------------------------------------------------------
// Do not add unnecessary info if current naming (from ContactName)
// coinsides with the generic name (that of Contact).
//-----------------------------------------------------------------
senderName = CleanedName( senderName );
if( senderName == contact.DisplayName )
return;
//-----------------------------------------------------------------
// Do not create ContactName if there already exists one with
// such senderName and it is linked to these account and contact.
//-----------------------------------------------------------------
bool multipleNamesFound = false;
IResource contactName = null;
IResourceList temp = RStore.EmptyResourceList;
if( accnt != null )
temp = accnt.GetLinksOfType( "ContactName", Props.LinkEmailAcct );
// use the fact that usually there is very small amount of ContactNames
// linked to a particular email account, thus the iteration over it
// is cheap.
foreach( IResource res in temp )
{
if( res.GetStringProp( Core.Props.Name ) == senderName &&
res.HasLink( Props.LinkBaseContact, contact ) )
{
multipleNamesFound = (contactName != null);
contactName = res;
}
}
//-----------------------------------------------------------------
// Internal consistency check - account can not be connected to more
// than one ContactName having equal sender names.
//-----------------------------------------------------------------
int nameLinkId = GetNameLinkId( linkId );
if( multipleNamesFound )
{
// If so - we deal with previous database version which had a bug in the
// CNames creation. In such case we can link to ANY CName resource.
Trace.WriteLine( "ContactManager -- ContactName config violation: more than one equal CName [" +
senderName + "] for an account [" + accnt.DisplayName + "]" );
}
//-----------------------------------------------------------------
// If there is only one ContactName, then it was created before,
// link the mail to this CName, do not create extra one.
//-----------------------------------------------------------------
if( contactName != null )
mail.AddLink( nameLinkId, contactName );
else
{
//-------------------------------------------------------------
// Some information sources (e.g. RSS feeds) do not provide an
// account at all. Creating new CName thus will lead
// to creation of numerous equal CName resources linked to each
// RSS post - instead, we must take the one which already exists
// without any linkage to accounts.
//-------------------------------------------------------------
if( accnt == null )
{
IResourceList linkedCNames = contact.GetLinksOfType( "ContactName", Props.LinkBaseContact );
foreach( IResource name in linkedCNames )
{
if( name.GetStringProp( Core.Props.Name ) == senderName )
{
mail.AddLink( nameLinkId, name );
return;
}
}
}
// No such CName linked by any configuration - create new one.
CreateNewContactName( contact, accnt, mail, nameLinkId, senderName );
}
}
private static void CreateNewContactName( IResource contact, IResource account,
IResource mail, int linkId, string name )
{
IResource contactName = RStore.BeginNewResource( "ContactName" );
if( !String.IsNullOrEmpty( name ) )
contactName.SetProp( "Name", name );
else
Trace.WriteLineIf( !IsTraceSuppressed, "ContactManager -- Creating CN with empty name" );
mail.AddLink( linkId, contactName );
contactName.AddLink( Core.ContactManager.Props.LinkBaseContact, contact );
if( account != null )
contactName.AddLink( Core.ContactManager.Props.LinkEmailAcct, account );
contactName.EndUpdate();
}
//---------------------------------------------------------------------
// Removal of email account from contact (unlinking of the former and
// the latter) requires more actions than just removal of the direct
// connection:
// - mails which are linked to these contact and account have to be
// relinked to a new contact linked with the account.
// - this new contact is created anew with the name derived from the
// linked ContactName.
// - ContactNames which were linked to the source contact are now
// relinked to the new contact.
//---------------------------------------------------------------------
public void HardRemoveAccountFromContact( IResource contact, IResource account )
{
#region Preconditions
if( contact == null )
throw new ArgumentNullException( "contact", "ContactManager -- contact resource can not be null" );
if( contact.Type != "Contact" )
throw new ArgumentException( "ContactManager -- first resource parameter has inproper type (Contact is expected)" );
if( account == null )
throw new ArgumentNullException( "account", "ContactManager -- email account string can not be null or empty" );
if( account.Type != "EmailAccount" )
throw new ArgumentException( "ContactManager -- second resource parameter has inproper type (EmailAccount is expected)" );
#endregion Preconditions
Trace.WriteLine( "ContactManager -- Unlinking account [" + account.DisplayName + "] from contact [" + contact.DisplayName + "]");
contact.BeginUpdate();
contact.DeleteLink( _propDefaultAccount, account );
contact.DeleteLink( Props.LinkEmailAcct, account );
// Collect all mails linked both to this contact and this account
IResourceList mails = GetMailsLinkedToContactAndAccount( contact, account );
int count = mails.Count;
if( count > 0 )
{
int percent = 0;
Hashtable names2Contact = new Hashtable();
for( int i = 0; i < count; i++ )
{
IResource mail = mails[ i ];
// A mail can be linked to an account with several types
// of links. For example, when a person sends a mail and puts
// himself to CC. In such case we need to enumerate all links.
int[] accountLink = GetAllAccountLinks( mail, account );
foreach( int link in accountLink )
RelinkContactFromMail( link, mail, account, contact, ref names2Contact );
if( i * 100 / count != percent && Core.ProgressWindow != null )
{
percent = i * 100 / count;
Core.ProgressWindow.UpdateProgress( percent, "Updating correspondence", null );
Trace.WriteLine( "ContactManager -- " + percent + " of correspondence processed." );
}
}
}
contact.EndUpdate();
}
private void RelinkContactFromMail( int accntLinkId, IResource mail, IResource account,
IResource contact, ref Hashtable names2Contact )
{
// Since the mail can be linked to the Contact by all From,
// To and CC links, use that corresponding to the link type from
// mail to account.
int majorLinkId = GetMajorLinkId( accntLinkId );
int nameLinkId = GetNameLinkId( majorLinkId );
// We iterate over emails linked to the old contact and the
// email account. But we can not guarantee that the CURRENT
// link between email and account is one of several which do
// not correspond to the contact under deletion.
if( mail.HasLink( majorLinkId, contact ) )
{
string name = null;
IResource contactName = GetContactNameLinkedToContactAndAccount( mail, contact, account, nameLinkId );
if( contactName != null )
name = contactName.DisplayName;
else
name = "From: " + account.DisplayName;
IContact newContact = CreateNewContact( account, names2Contact, name );
Trace.WriteLine( "ContactManager -- Created (used) new account with name [" + name + "]" );
// Retarget mail, contact name and potentially account.
mail.DeleteLink( majorLinkId, contact );
mail.AddLink( majorLinkId, newContact.Resource );
if( contactName != null )
contactName.SetProp( Props.LinkBaseContact, newContact.Resource );
else
CreateAndLinkContactName( newContact.Resource, mail, account, majorLinkId, name );
if( !account.HasLink( Props.LinkEmailAcct, newContact.Resource ) )
account.AddLink( Props.LinkEmailAcct, newContact.Resource );
}
}
//---------------------------------------------------------------------
// Collect all mails linked both to a contact and an account
//---------------------------------------------------------------------
private static IResourceList GetMailsLinkedToContactAndAccount( IResource contact, IResource account )
{
IResourceList temp = LinkedAccountCorrespondence( account );
return LinkedCorrespondence( contact ).Intersect( temp, true );
}
private static IContact CreateNewContact( IResource account, Hashtable names2Contact, string name )
{
IContact contact = (IContact) names2Contact[ name ];
if( contact == null )
{
contact = Core.ContactManager.CreateOrUpdateContact( account, name );
names2Contact[ name ] = contact;
}
return contact;
}
//---------------------------------------------------------------------
// Get a ContactName resource linked to the particular mail, contact
// and an account by the particular link Id. We have to intersect all
// resource list since the same mail can be sent to the same Person
// under several accounts/contacts with the same linkId.
//---------------------------------------------------------------------
private IResource GetContactNameLinkedToContactAndAccount( IResource mail, IResource contact, IResource account, int nameLinkId )
{
IResourceList names = mail.GetLinksOfType( "ContactName", nameLinkId );
names = names.Intersect( contact.GetLinksOfType( "ContactName", Props.LinkBaseContact ), true );
names = names.Intersect( account.GetLinksOfType( "ContactName", Props.LinkEmailAcct ), true );
return (names.Count > 0)? names[ 0 ] : null;
}
public static void CloneLinkage( IResource fromRes, IResource toRes )
{
AssignLinksOfType( fromRes, toRes, "Contact", Core.ContactManager.Props.LinkFrom );
AssignLinksOfType( fromRes, toRes, "Contact", Core.ContactManager.Props.LinkTo );
AssignLinksOfType( fromRes, toRes, "Contact", Core.ContactManager.Props.LinkCC );
AssignLinksOfType( fromRes, toRes, "EmailAccount", Core.ContactManager.Props.LinkEmailAcctFrom );
AssignLinksOfType( fromRes, toRes, "EmailAccount", Core.ContactManager.Props.LinkEmailAcctTo );
AssignLinksOfType( fromRes, toRes, "EmailAccount", Core.ContactManager.Props.LinkEmailAcctCC );
AssignLinksOfType( fromRes, toRes, "ContactName", Core.ContactManager.Props.LinkNameFrom );
AssignLinksOfType( fromRes, toRes, "ContactName", Core.ContactManager.Props.LinkNameTo );
AssignLinksOfType( fromRes, toRes, "ContactName", Core.ContactManager.Props.LinkNameCC );
}
private static void AssignLinksOfType( IResource fromRes, IResource toRes, string resType, int linkId )
{
IResourceList list = fromRes.GetLinksOfType( resType, linkId );
if( list.Count > 0 )
{
toRes.BeginUpdate();
foreach( IResource res in list )
{
// Make a workaround for corrupted databases, OM-13896.
try { toRes.AddLink( linkId, res ); }
catch( Exception ) {}
}
toRes.EndUpdate();
}
}
#endregion Contact Linkage
#region Auxiliary
public bool IsMajorLink( int propId )
{
return (propId == Props.LinkFrom) || (propId == Props.LinkTo) || (propId == Props.LinkCC);
}
public bool IsNameLink( int propId )
{
return (propId == Props.LinkNameFrom) || (propId == Props.LinkNameTo) || (propId == Props.LinkNameCC);
}
public int GetNameLinkId( int propId )
{
#region Preconditions
if( !IsMajorLink( propId ) )
throw new ArgumentException( "ContactManager -- invalid link Id parameter" );
#endregion Preconditions
int resultId = Props.LinkNameCC;
if( propId == Props.LinkFrom )
resultId = Props.LinkNameFrom;
else
if( propId == Props.LinkTo )
resultId = Props.LinkNameTo;
return resultId;
}
public int GetAccountLinkId( int propId )
{
#region Preconditions
if( !IsMajorLink( propId ) )
throw new ArgumentException( "ContactManager -- invalid link Id parameter" );
#endregion Preconditions
int resultId = Props.LinkEmailAcctCC;
if( propId == Props.LinkFrom )
resultId = Props.LinkEmailAcctFrom;
else
if( propId == Props.LinkTo )
resultId = Props.LinkEmailAcctTo;
return resultId;
}
private int GetMajorLinkId( int propId )
{
if( propId == Props.LinkEmailAcctFrom )
return Props.LinkFrom;
else
if( propId == Props.LinkEmailAcctTo )
return Props.LinkTo;
else
if( propId == Props.LinkEmailAcctCC )
return Props.LinkCC;
else
throw new ArgumentException( "ContactManager -- invalid Account link Id parameter" );
}
private int[] GetAllAccountLinks( IResource mail, IResource account )
{
IntArrayList links = IntArrayListPool.Alloc();
try
{
if( mail.HasLink( Props.LinkEmailAcctFrom, account ) )
links.Add( Props.LinkEmailAcctFrom );
if( mail.HasLink( Props.LinkEmailAcctTo, account ) )
links.Add( Props.LinkEmailAcctTo );
if( mail.HasLink( Props.LinkEmailAcctCC, account ) )
links.Add( Props.LinkEmailAcctCC );
return links.ToArray();
}
finally
{
IntArrayListPool.Dispose( links );
}
}
private static bool IsUniqueLink( int propId, string fromRT )
{
return (RStore.GetMaxLinkCountRestriction( fromRT, propId ) == 1);
}
public bool ResolveName( string fullName, string emailAccount,
out string title, out string firstName,
out string midName, out string lastName, string suffix )
{
string addSpec;
return ContactResolver.ResolveName( fullName, emailAccount, out title, out firstName,
out midName, out lastName, out suffix, out addSpec );
}
public static int GetLinkedIdFromContactName( IResource cName )
{
#region Preconditions
if( cName.Type != "ContactName" )
throw new ArgumentException( "ContactManager -- invalid argument type [" + cName.Type + "] - ContactName expected." );
#endregion Preconditions
if( cName.GetLinkCount( -Core.ContactManager.Props.LinkFrom ) > 0 )
return Core.ContactManager.Props.LinkFrom;
else
if( cName.GetLinkCount( -Core.ContactManager.Props.LinkTo ) > 0 )
return Core.ContactManager.Props.LinkTo;
else
if( cName.GetLinkCount( -Core.ContactManager.Props.LinkCC ) > 0 )
return Core.ContactManager.Props.LinkCC;
else
throw new InvalidConstraintException( "ContactManager -- Found a ContactName resource which is not linked to a primary resource." );
}
private IResource FindBestCandidate( IResourceList contacts )
{
int maxMails = -1, maxIndex = -1;
for( int i = 0; i < contacts.Count; i++ )
{
int count = contacts[ i ].GetLinkCount( Props.LinkFrom ) +
contacts[ i ].GetLinkCount( Props.LinkTo ) +
contacts[ i ].GetLinkCount( Props.LinkCC );
if( count > maxMails )
{
maxMails = count;
maxIndex = i;
}
}
return contacts[ maxIndex ];
}
public IContact GetContact( IResource res )
{
return new ContactBO( res );
}
public string GetFullName( IResource res )
{
#region Preconditions
if( res == null )
throw new ArgumentNullException( "res", "ContactManager -- input contact can not be null" );
if( res.Type != "Contact" )
throw new ArgumentException( "ContactManager -- input parameter has unexpected resource type." );
#endregion Preconditions
return new ContactBO( res ).FullName;
}
///
/// Return a resource list of all the correspondence linked to this
/// contact via "From", "To" or "CC" links.
/// NB: the method can be used in reversed order - given a
/// correspondence resource method will return all contacts linked to
/// it via "From", "To" or "CC" links.
///
/// A contact resource.
/// Correspondence resources.
public static IResourceList LinkedCorrespondence( IResource contact )
{
IResourceList linked = contact.GetLinksOfType( null, Core.ContactManager.Props.LinkCC ).Union(
contact.GetLinksOfType( null, Core.ContactManager.Props.LinkTo ).Union(
contact.GetLinksOfType( null, Core.ContactManager.Props.LinkFrom ), true ), true );
return linked;
}
///
/// Return a resource list of all the correspondence linked to this
/// contact via "From" and "To" (no "CC") links.
/// NB: the method can be used in reversed order - given a
/// correspondence resource method will return all contacts linked to
/// it via "From" and "To" links.
///
/// A contact resource.
/// Correspondence resources.
public static IResourceList LinkedCorrespondenceDirect( IResource contact )
{
IResourceList linked = contact.GetLinksOfType( null, Core.ContactManager.Props.LinkFrom ).Union(
contact.GetLinksOfType( null, Core.ContactManager.Props.LinkTo ));
return linked;
}
///
/// Return a resource list of all the correspondence linked to this
/// contact name via "NameFrom", "NameTo" or "NameCC" links.
/// NB: the method can be used in reversed order - given a
/// correspondence resource method will return all contact names
/// linked to it via "NameFrom", "NameTo" or "NameCC" links.
///
/// A contact name resource.
/// Correspondence resources.
private static IResourceList LinkedNameCorrespondence( IResource res )
{
IResourceList linked = res.GetLinksOfType( null, Core.ContactManager.Props.LinkNameCC ).Union(
res.GetLinksOfType( null, Core.ContactManager.Props.LinkNameTo ).Union(
res.GetLinksOfType( null, Core.ContactManager.Props.LinkNameFrom )));
return linked;
}
///
/// Return a resource list of all the correspondence linked to this
/// account name via "AcctFrom", "AcctTo" or "AcctCC" links.
/// NB: the method can be used in reversed order - given a
/// correspondence resource method will return all accounts
/// linked to it via "AcctFrom", "AcctTo" or "AcctCC" links.
///
/// An account resource.
/// Correspondence resources.
public static IResourceList LinkedAccountCorrespondence( IResource account )
{
IResourceList linked = account.GetLinksOfType( null, Core.ContactManager.Props.LinkEmailAcctCC ).Union(
account.GetLinksOfType( null, Core.ContactManager.Props.LinkEmailAcctTo ).Union(
account.GetLinksOfType( null, Core.ContactManager.Props.LinkEmailAcctFrom )));
return linked;
}
///
/// Return a resource list of all accounts linked to this
/// contact via "EmailAccnt" link.
///
/// An contact resource.
/// Account resources.
public static IResourceList LinkedAccounts( IResource contact )
{
return contact.GetLinksOfType( "EmailAccount", Core.ContactManager.Props.LinkEmailAcct );
}
public static IResourceList LinkedAccounts( IResourceList contacts )
{
IResourceList accounts = Core.ResourceStore.EmptyResourceList;
foreach( IResource res in contacts )
{
accounts = accounts.Union( LinkedAccounts( res ), true );
}
return accounts;
}
private static String CleanedName( String name )
{
if( name != null )
{
if( name.Length > 2 && name[ 0 ]=='\'' && name[ name.Length - 1 ]=='\'' )
name = name.Substring( 1, name.Length - 2 ).Trim();
if( name.Length > 2 && name[ 0 ]=='"' && name[ name.Length - 1 ]=='"' )
name = name.Substring( 1, name.Length - 2 ).Trim();
if( name[ 0 ] == '\n' )
name = name.Substring( 1, name.Length - 1 ).Trim();
}
return name;
}
#endregion Auxiliary
#region Myself Contact
public IContact MySelf
{
get
{
if ( _mySelfContact == null )
{
Core.ResourceAP.RunUniqueJob( new MethodInvoker( CreateMyselfContact ) );
}
lock( this )
{
//-------------------------------------------------------------
// Create a resource list which tracks the removal of the MySelf
// contact, e.g. in the case of merging and splitting.
//-------------------------------------------------------------
if( _MySelfTrackingList == null )
{
_MySelfTrackingList = _mySelfContact.Resource.ToResourceListLive();
_MySelfTrackingList.ResourceDeleting += _MySelfTrackingList_ResourceDeleting;
}
}
// return new ContactBO( _mySelfContact.Resource );
return _mySelfContact;
}
}
private void CreateMyselfContact()
{
Trace.WriteLine( "ContactManager -- Myself contact is empty" );
MergeSeveralMyselfIfAny();
IResourceList mySelves = RStore.FindResources( "Contact", "MySelf", 1 );
if( mySelves.Count == 0 )
{
CreateMySelfResource();
mySelves = RStore.FindResources( "Contact", "MySelf", 1 );
}
if( RStore.PropTypes.Exist( "LastCorrespondDate" ) )
{
int propId = Core.ResourceStore.PropTypes[ "LastCorrespondDate" ].Id;
mySelves.Sort( new SortSettings( propId, false ) );
}
int index = 0;
for ( int i = 0; i < mySelves.Count ; i++ )
{
if ( mySelves[ i ].HasProp( "LastCorrespondDate" ) )
{
index = i;
break;
}
}
_mySelfContact = new ContactBO( mySelves[ index ] );
// Still empty FN and LN? It means that we have to take
// information from the Environment and cross the fingers
// that it is valid.
if( _mySelfContact.FirstName == string.Empty &&
_mySelfContact.LastName == string.Empty )
{
Trace.WriteLine( "ContactManager -- FN and LN are still empty - analyzing log name" );
string userName = SystemInformation.UserName;
if( !string.IsNullOrEmpty( userName ))
{
Trace.WriteLine( "ContactManager -- Log name is valid - [" + userName + "]" );
string title, fName, midName, lName, suffix, addSpec;
ExtractPossibleFields( userName, out title, out fName, out midName, out lName, out suffix, out addSpec );
_mySelfContact.Title = title;
_mySelfContact.FirstName = fName;
_mySelfContact.MiddleName = midName;
_mySelfContact.LastName = lName;
_mySelfContact.Suffix = suffix;
_mySelfContact.QueueIndexing();
Trace.WriteLine( "ContactManager -- Result fields are: [" + title + "][" + fName + "][" +
midName + "][" + lName + "][" + suffix + "][" + addSpec + "]");
}
}
}
private static void CreateMySelfResource()
{
//-----------------------------------------------------------------
// We have to check the presence of the MySelf contact once more
// because of cases when caller's getter is called several times
// and myself resource is not created yet.
// Thus we check here whether previous Job has just created the
// necessary object and do not create the second copy.
//-----------------------------------------------------------------
IResourceList mySelves = RStore.FindResources( "Contact", "MySelf", 1 );
if( mySelves.Count == 0 )
{
IResource myself = RStore.NewResource( "Contact" );
myself.SetProp( "MySelf", 1 );
}
}
private void MergeSeveralMyselfIfAny()
{
IResourceList mySelves = RStore.FindResources( "Contact", "MySelf", 1 );
if( mySelves.Count > 1 )
{
if( RStore.PropTypes.Exist( "LastCorrespondDate" ) )
{
int propId = Core.ResourceStore.PropTypes[ "LastCorrespondDate" ].Id;
mySelves.Sort( new SortSettings( propId, false ) );
}
ContactBO target = new ContactBO( mySelves[ 0 ] );
for( int i = 1; i < mySelves.Count; i++ )
MergeData( new ContactBO( mySelves[ i ] ), target );
for( int i = 1; i < mySelves.Count; i++ )
{
mySelves[ i ].Delete();
}
if( RStore.FindResources( "Contact", "MySelf", 1 ).Count != 1 )
throw new ApplicationException( "Merging exception: Myself count still out of range" );
}
}
// Mark the variable _mySelfContact as null so that it will be
// recreated on the next getter call.
private void _MySelfTrackingList_ResourceDeleting(object sender, ResourceIndexEventArgs e)
{
_mySelfContact = null;
_MySelfTrackingList.ResourceDeleting -= _MySelfTrackingList_ResourceDeleting;
_MySelfTrackingList = null;
_myselfAccounts = Core.ResourceStore.EmptyResourceList;
}
// Implement simple heuristic - if user name is a compound of two
// (valid) strings delimited by the dot, use compounds as first name
// and last name correspondingly.
private static void ExtractPossibleFields( string name,
out string title, out string fName, out string midName,
out string lName, out string suffix, out string addSpec )
{
string[] dotDelimitedFields = name.Split( '.' );
if( dotDelimitedFields.Length == 2 &&
!string.IsNullOrEmpty( dotDelimitedFields[ 0 ] ) && !string.IsNullOrEmpty( dotDelimitedFields[ 1 ] ))
{
fName = dotDelimitedFields[ 0 ];
lName = dotDelimitedFields[ 1 ];
title = midName = suffix = addSpec = "";
}
else
{
ContactResolver.ResolveName( name, null, out title, out fName,
out midName, out lName, out suffix, out addSpec );
}
}
#endregion Myself Contact
}
#region FiltersAndActions
public class ItemRecipientsFilter : ILinksPaneFilter
{
private readonly ContactManager _contactMgr;
public ItemRecipientsFilter()
{
_contactMgr = Core.ContactManager as ContactManager;
}
public bool AcceptLinkType(IResource baseRes, int propId, ref string displayName)
{
return !_contactMgr.IsNameLink(propId);
}
public bool AcceptLink(IResource baseRes, int propId, IResource contact, ref string linkTooltip)
{
#region Preconditions
if( baseRes == null )
throw new ArgumentNullException( "baseRes", "ContactManager -- Source object is null in filter.");
if( contact == null )
throw new ArgumentNullException( "contact", "ContactManager -- Target contact is null in filter.");
#endregion Preconditions
if (_contactMgr.IsMajorLink(propId) && contact.Type == "Contact")
{
bool showOrig = contact.HasProp(_contactMgr.Props.ShowOriginalNames);
int nameLinkId = _contactMgr.GetNameLinkId(propId);
int accountLinkId = _contactMgr.GetAccountLinkId(propId);
IResource cName;
IResourceList contactAccounts = contact.GetLinksOfType(null, _contactMgr.Props.LinkEmailAcct);
IResourceList mailAccounts = baseRes.GetLinksOfType(null, accountLinkId);
// Link to direct contact may be rejected if a contact requires
// showing of original name, and there is such ContactName for
// that contact linked to source resource.
if (showOrig && (cName = GetContactNamebyPair(baseRes, nameLinkId, contact)) != null)
{
linkTooltip = cName.GetStringProp(Core.Props.Name);
if (contactAccounts.Count == 1)
linkTooltip = linkTooltip + " <" + contactAccounts[0].DisplayName + ">";
}
else
{
mailAccounts = mailAccounts.Intersect(contactAccounts, true);
if (mailAccounts.Count > 0)
{
linkTooltip = string.Empty;
foreach (IResource account in mailAccounts)
linkTooltip += contact.DisplayName + " <" + account.DisplayName + ">";
}
}
}
return true;
}
public bool AcceptAction(IResource displayedResource, IAction action)
{
return true;
}
private IResource GetContactNamebyPair(IResource mail, int nameLinkId, IResource contact)
{
IResourceList cNames = mail.GetLinksOfType(null, nameLinkId);
foreach (IResource name in cNames)
{
if (name.HasLink(_contactMgr.Props.LinkBaseContact, contact))
return name;
}
return null;
}
}
#endregion FiltersAndActions
}