/// Copyright 2016 Kopano b.v. /// /// This program is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License, version 3, /// as published by the Free Software Foundation. /// /// This program is distributed in the hope that it will be useful, /// but WITHOUT ANY WARRANTY; without even the implied warranty of /// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the /// GNU Affero General Public License for more details. /// /// You should have received a copy of the GNU Affero General Public License /// along with this program.If not, see. /// /// Consult LICENSE file for details using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Acacia.Stubs; using Acacia.ZPush; using Acacia.Utils; using System.Collections; using static Acacia.DebugOptions; namespace Acacia.Features.GAB { public class GABHandler : LogContext { public string LogContextId { get { return "GAB"; } } private readonly FeatureGAB _feature; #region Contacts private readonly Func _contactsProvider; private readonly Action _contactsDisposer; private IAddressBook _contacts; public IAddressBook Contacts { get { if (_contacts == null) { _contacts = _contactsProvider(Folder); } return _contacts; } } private IStorageItem GetIndexItem() { return Contacts?.GetStorageItem(Constants.ZPUSH_GAB_INDEX); } #endregion #region Accounts & Folders public ZPushAccount ActiveAccount { get { return ThisAddIn.Instance.Watcher.Accounts.GetAccount(Folder); } } /// /// The list of accounts that are associated with this GAB. /// private readonly List _accounts = new List(); private readonly List _accountFolders = new List(); private IFolder Folder { get { if (!HasAccounts) return null; return _accountFolders.FirstOrDefault(); } } public void AddAccount(ZPushAccount account, IFolder folder) { _accounts.Add(account); _accountFolders.Add(folder); } public void RemoveAccount(ZPushAccount account) { int i = _accounts.IndexOf(account); if (i >= 0) { _accounts.RemoveAt(i); _accountFolders.RemoveAt(i); } } internal bool HasAccounts { get { return _accounts.Count > 0; } } #endregion public GABHandler(FeatureGAB feature, Func contactsProvider, Action contactsDisposer) { this._feature = feature; this._contactsProvider = contactsProvider; this._contactsDisposer = contactsDisposer; } public string DisplayName { get { using(IStore store = Folder.Store) return store.DisplayName; } } #region Processing public void FullResync() { ClearContacts(); Process(null); } private void ClearContacts() { if (Contacts != null) { try { Contacts.Delete(); } catch (Exception e) { Logger.Instance.Warning(this, "Error clearing contacts folder for {0}: {1}", DisplayName, e); // There was an error deleting the contacts folder, try clearing it using (IStorageItem index = GetIndexItem()) { index?.Delete(); } Contacts.Clear(); } CleanupContactsObject(); } } /// /// Processes the GAB message(s). /// /// If specified, this item has changed. If null, means a global check should be performed public void Process(IZPushItem item) { try { if (item == null) { if (Folder != null) ProcessMessages(); } else { ProcessMessage(item); } } catch(Exception e) { Logger.Instance.Error(this, "Exception in GAB.Process: {0}", e); } } private void ProcessMessages() { if (!_feature.ProcessFolder) return; DetermineSequence(); if (CurrentSequence == null) return; // No messages to process if (!_feature.ProcessItems) return; // Process the messages foreach (IItem item in Folder.Items) { // TODO: make type-checking iterator? if (item is IZPushItem) { string entryId = item.EntryId; Logger.Instance.Trace(this, "Checking chunk: {0}", item.Subject); if (_feature.ProcessItems2) { Tasks.Task(_feature, "ProcessChunk", () => { using (IItem item2 = Folder.GetItemById(entryId)) { if (item2 != null) ProcessMessage((IZPushItem)item2); } }); } } } } public const string PROP_LAST_PROCESSED = "ZPushLastProcessed"; public const string PROP_SEQUENCE = "ZPushSequence"; public const string PROP_CHUNK = "ZPushChunk"; public const string PROP_GAB_ID = "ZPushId"; public const string PROP_CURRENT_SEQUENCE = "ZPushCurrentSequence"; private void ProcessMessage(IZPushItem item) { if (!_feature.ProcessMessage) return; // Check if the message is for the current sequence ChunkIndex? optionalIndex = ChunkIndex.Parse(item.Subject); if (optionalIndex == null) { Logger.Instance.Trace(this, "Not a chunk: {0}", item.Subject); return; } if (optionalIndex.Value.numberOfChunks != CurrentSequence) { // Try to update the current sequence; this message may indicate that it has changed DetermineSequence(); // If it is still not for the current sequence, it's an old message if (optionalIndex.Value.numberOfChunks != CurrentSequence) { Logger.Instance.Trace(this, "Skipping, wrong sequence: {0}", item.Subject); return; } } ChunkIndex index = optionalIndex.Value; // Check if the message is up to date string lastProcessed = GetChunkStateString(index); if (lastProcessed == item.Location) { Logger.Instance.Trace(this, "Already up to date: {0} - {1}", item.Subject, item.Location); return; } // Process it Logger.Instance.Trace(this, "Processing: {0} - {1} - {2}", item.Subject, item.Location, lastProcessed); _feature?.BeginProcessing(); try { // Delete the old contacts from this chunk ISearch search = Contacts.Search(); search.AddField(PROP_SEQUENCE, true).SetOperation(SearchOperation.Equal, index.numberOfChunks); search.AddField(PROP_CHUNK, true).SetOperation(SearchOperation.Equal, index.chunk); foreach (IItem oldItem in search.Search()) { // TODO: Search should handle this, like folder enumeration using (oldItem) { Logger.Instance.Trace(this, "Deleting GAB entry: {0}", oldItem.Subject); oldItem.Delete(); } } // Create the new contacts ProcessChunkBody(item, index); // Update the state SetChunkStateString(index, item.Location); } finally { _feature?.EndProcessing(); } } #endregion #region Sequence public int? CurrentSequence { get { using (IStorageItem index = GetIndexItem()) { return index?.GetUserProperty(PROP_CURRENT_SEQUENCE)?.Value; } } set { using (IStorageItem index = GetIndexItem()) { if (value != null) { index.GetUserProperty(PROP_CURRENT_SEQUENCE, true).Value = value.Value; } else { index.Delete(); } } } } private IItem FindNewestChunk() { if (Folder == null) return null; // Scan a few of the newest items, in case there is some junk in the ZPush folder // TODO: this shouldn't happen in production. int i = 0; foreach(IItem item in Folder.ItemsSorted("LastModificationTime", true)) { if (ChunkIndex.Parse(item.Subject) != null) return item; item.Dispose(); if (i > Constants.ZPUSH_GAB_NEWEST_MAX_CHECK) return null; ++i; } return null; } public void DetermineSequence() { try { // Find the newest chunk using (IItem newest = FindNewestChunk()) { if (newest == null) CurrentSequence = null; else { Logger.Instance.Trace(this, "Newest chunk: {0}", newest.Subject); ChunkIndex? newestChunkIndex = ChunkIndex.Parse(newest.Subject); if (!CurrentSequence.HasValue || CurrentSequence.Value != newestChunkIndex?.numberOfChunks) { // Sequence has changed. Delete contacts Logger.Instance.Trace(this, "Rechunked, deleting contacts"); ClearContacts(); // Determine new sequence if (newestChunkIndex == null) { using (IStorageItem index = GetIndexItem()) { if (index != null) index.Delete(); } } else { int numberOfChunks = newestChunkIndex.Value.numberOfChunks; using (IStorageItem index = GetIndexItem()) { index.GetUserProperty(PROP_CURRENT_SEQUENCE, true).Value = numberOfChunks; index.GetUserProperty(PROP_LAST_PROCESSED, true).Value = CreateChunkStateString(numberOfChunks); index.Save(); } } } } } } catch(Exception e) { Logger.Instance.Trace(this, "Exception determining sequence: {0}", e); // Delete the index item using (IStorageItem index = GetIndexItem()) index?.Delete(); return; } Logger.Instance.Trace(this, "Current sequence: {0}", CurrentSequence); } private string CreateChunkStateString(int count) { string[] defaultValues = new string[count]; return string.Join(";", defaultValues); } private string GetChunkStateString(ChunkIndex index) { using (IStorageItem item = GetIndexItem()) { if (item == null) return null; string state = item.GetUserProperty(PROP_LAST_PROCESSED)?.Value; if (string.IsNullOrEmpty(state)) return null; string[] parts = state.Split(';'); if (parts.Length != index.numberOfChunks) { Logger.Instance.Error(this, "Wrong number of chunks, got {0}, expected {1}: {2}", parts.Length, index.numberOfChunks, state); } return parts[index.chunk]; } } private void SetChunkStateString(ChunkIndex index, string partState) { using (IStorageItem item = GetIndexItem()) { string state = item.GetUserProperty(PROP_LAST_PROCESSED)?.Value; string[] parts; if (string.IsNullOrEmpty(state)) parts = new string[index.numberOfChunks]; else parts = state.Split(';'); if (parts.Length != index.numberOfChunks) { Logger.Instance.Error(this, "Wrong number of chunks, got {0}, expected {1}: {2}", parts.Length, index.numberOfChunks, state); } parts[index.chunk] = partState; string combined = string.Join(";", parts); item.GetUserProperty(PROP_LAST_PROCESSED, true).Value = combined; item.Save(); } } #endregion #region Message parsing private ValueType Get(Dictionary values, string name) where ValueType : class { object value; values.TryGetValue(name, out value); return value as ValueType; } private void ProcessChunkBody(IZPushItem item, ChunkIndex index) { // Process the body foreach (var entry in JSONUtils.Deserialise(item.Body)) { string id = entry.Key; Dictionary value = (Dictionary)entry.Value; Tasks.Task(_feature, "CreateItem", () => CreateObject(index, id, value)); } } private void CreateObject(ChunkIndex index, string id, Dictionary value) { try { _feature?.BeginProcessing(); string type = Get(value, "type"); if (type == "contact") CreateContact(id, value, index, 0); else if (type == "group") CreateGroup(id, value, index); else if (type == "equipment") CreateContact(id, value, index, OutlookConstants.DT_EQUIPMENT); else if (type == "room") CreateContact(id, value, index, OutlookConstants.DT_ROOM); else { Logger.Instance.Warning(this, "Unknown entry type: {0}", type); } } catch (System.Exception e) { Logger.Instance.Error(this, "Error creating entry: {0}: {1}", id, e); } finally { _feature?.EndProcessing(); } } private void CreateContact(string id, Dictionary value, ChunkIndex index, int resourceType) { if (!_feature.CreateContacts) return; using (IContactItem contact = Contacts.Create()) { Logger.Instance.Trace(this, "Creating contact: {0}", id); contact.CustomerID = id; // Create the contact data if (Get(value, "displayName") != null) contact.FullName = Get(value, "displayName"); if (Get(value, "givenName") != null) contact.FirstName = Get(value, "givenName"); if (Get(value, "initials") != null) contact.Initials = Get(value, "initials"); if (Get(value, "surname") != null) contact.LastName = Get(value, "surname"); if (Get(value, "title") != null) contact.JobTitle = Get(value, "title"); if (Get(value, "smtpAddress") != null) { contact.Email1Address = Get(value, "smtpAddress"); contact.Email1AddressType = "SMTP"; } if (Get(value, "companyName") != null) contact.CompanyName = Get(value, "companyName"); if (Get(value, "officeLocation") != null) contact.OfficeLocation = Get(value, "officeLocation"); if (Get(value, "businessTelephoneNumber") != null) contact.BusinessTelephoneNumber = Get(value, "businessTelephoneNumber"); if (Get(value, "mobileTelephoneNumber") != null) contact.MobileTelephoneNumber = Get(value, "mobileTelephoneNumber"); if (Get(value, "homeTelephoneNumber") != null) contact.HomeTelephoneNumber = Get(value, "homeTelephoneNumber"); if (Get(value, "beeperTelephoneNumber") != null) contact.PagerNumber = Get(value, "beeperTelephoneNumber"); if (Get(value, "primaryFaxNumber") != null) contact.BusinessFaxNumber = Get(value, "primaryFaxNumber"); if (Get(value, "organizationalIdNumber") != null) contact.OrganizationalIDNumber = Get(value, "organizationalIdNumber"); if (Get(value, "postalAddress") != null) contact.BusinessAddress = Get(value, "postalAddress"); if (Get(value, "businessAddressCity") != null) contact.BusinessAddressCity = Get(value, "businessAddressCity"); if (Get(value, "businessAddressPostalCode") != null) contact.BusinessAddressPostalCode = Get(value, "businessAddressPostalCode"); if (Get(value, "businessAddressPostOfficeBox") != null) contact.BusinessAddressPostOfficeBox = Get(value, "businessAddressPostOfficeBox"); if (Get(value, "businessAddressStateOrProvince") != null) contact.BusinessAddressState = Get(value, "businessAddressStateOrProvince"); if (Get(value, "language") != null) contact.Language = Get(value, "language"); // Thumbnail string photoData = Get(value, "thumbnailPhoto"); if (photoData != null) { string path = null; try { byte[] data = Convert.FromBase64String(photoData); path = System.IO.Path.GetTempFileName(); Logger.Instance.Trace(this, "Contact image: {0}", path); System.IO.File.WriteAllBytes(path, data); contact.SetPicture(path); } catch (Exception) { } finally { try { if (path != null) System.IO.File.Delete(path); } catch (Exception) { } } } // Resource flags if (resourceType != 0) { contact.SetProperty(OutlookConstants.PR_DISPLAY_TYPE, 0); contact.SetProperty(OutlookConstants.PR_DISPLAY_TYPE_EX, resourceType); } // Standard properties SetItemStandard(contact, id, value, index); contact.Save(); // Update the groups AddItemToGroups(contact, id, value, index); } } private void CreateGroup(string id, Dictionary value, ChunkIndex index) { if (!_feature.CreateGroups) return; using (IDistributionList group = Contacts.Create()) { Logger.Instance.Debug(this, "Creating group: {0}", id); group.DLName = Get(value, "displayName"); if (Get(value, "smtpAddress") != null) { group.SMTPAddress = Get(value, "smtpAddress"); } SetItemStandard(group, id, value, index); group.Save(); if (_feature.GroupMembers) { ArrayList members = Get(value, "members"); if (members != null) { foreach (string memberId in members) { using (IItem item = FindItemById(memberId)) { Logger.Instance.Debug(this, "Finding member {0} of {1}: {2}", memberId, id, item?.EntryId); if (item != null) AddGroupMember(group, item); } } } group.Save(); } AddItemToGroups(group, id, value, index); } } private IItem FindItemById(string id) { ISearch search = Contacts.Search(); search.AddField(PROP_GAB_ID, true).SetOperation(SearchOperation.Equal, id); return search.SearchOne(); } private void SetItemStandard(IItem item, string id, Dictionary value, ChunkIndex index) { // Set the chunk data item.GetUserProperty(PROP_SEQUENCE, true).Value = index.numberOfChunks; item.GetUserProperty(PROP_CHUNK, true).Value = index.chunk; item.GetUserProperty(PROP_GAB_ID, true).Value = id; } private void AddGroupMember(IDistributionList group, IItem item) { if (!_feature.GroupMembersAdd) return; if (item is IDistributionList) { if (!_feature.NestedGroups) return; } group.AddMember(item); } private void AddItemToGroups(IItem item, string id, Dictionary value, ChunkIndex index) { if (!_feature.GroupMembers) return; // Find the groups if (Get(value, "memberOf") != null) { ArrayList members = Get(value, "memberOf"); foreach (string memberOf in members) { using (IItem groupItem = FindItemById(memberOf)) { Logger.Instance.Debug(this, "Finding group {0} for {1}: {2}", memberOf, id, groupItem?.EntryId); if (groupItem is IDistributionList) { AddGroupMember((IDistributionList)groupItem, item); groupItem.Save(); } } } } } #endregion #region Removal public void Remove() { if (_contacts != null) { _contacts.Delete(); } CleanupContactsObject(); } private void CleanupContactsObject() { if (_contacts != null) { if (_contactsDisposer != null) _contactsDisposer(_contacts); _contacts.Dispose(); _contacts = null; } } #endregion } }