From d4c8ae6cb71fec1252cdc1697dc806748c9b2eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 25 Oct 2025 10:54:38 +0200 Subject: [PATCH] Attempt to bring back ListView. --- .../Interfaces/IMailHashContainer.cs | 9 + .../Models/Comparers/DateTimeComparer.cs | 15 - .../Models/Comparers/FolderNameComparer.cs | 12 - .../Models/Comparers/ListItemComparer.cs | 20 + .../Collections/WinoMailCollection.cs | 516 ++++++++++++++++++ Wino.Mail.ViewModels/Data/IMailListItem.cs | 25 + .../Data/MailItemViewModel.cs | 10 +- .../Data/ThreadMailItemViewModel.cs | 15 +- 8 files changed, 587 insertions(+), 35 deletions(-) create mode 100644 Wino.Core.Domain/Interfaces/IMailHashContainer.cs delete mode 100644 Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs delete mode 100644 Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs create mode 100644 Wino.Core.Domain/Models/Comparers/ListItemComparer.cs create mode 100644 Wino.Mail.ViewModels/Collections/WinoMailCollection.cs create mode 100644 Wino.Mail.ViewModels/Data/IMailListItem.cs diff --git a/Wino.Core.Domain/Interfaces/IMailHashContainer.cs b/Wino.Core.Domain/Interfaces/IMailHashContainer.cs new file mode 100644 index 00000000..9b4a0952 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IMailHashContainer.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace Wino.Core.Domain.Interfaces; + +public interface IMailHashContainer +{ + IEnumerable GetContainingIds(); +} diff --git a/Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs b/Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs deleted file mode 100644 index c5ae37a0..00000000 --- a/Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Wino.Core.Domain.Models.Comparers; - -/// -/// Used to insert date grouping into proper place in Reader page. -/// -public class DateTimeComparer : IComparer -{ - public int Compare(DateTime x, DateTime y) - { - return DateTime.Compare(y, x); - } -} diff --git a/Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs b/Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs deleted file mode 100644 index 440391f2..00000000 --- a/Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Entities.Mail; - -namespace Wino.Core.Domain.Models.Comparers; - -public class FolderNameComparer : IComparer -{ - public int Compare(MailItemFolder x, MailItemFolder y) - { - return x.FolderName.CompareTo(y.FolderName); - } -} diff --git a/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs b/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs new file mode 100644 index 00000000..7431f85d --- /dev/null +++ b/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; + +public class ListItemComparer : IComparer +{ + public bool SortByName { get; set; } + + public int Compare(object x, object y) + { + if (x is MailCopy xMail && y is MailCopy yMail) + return SortByName ? string.Compare(xMail.FromName, yMail.FromName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(yMail.CreationDate, xMail.CreationDate); + else if (x is DateTime dateX && y is DateTime dateY) + return DateTime.Compare(dateY, dateX); + else if (x is string stringX && y is string stringY) + return stringY.CompareTo(stringX); + + return 0; + } +} diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs new file mode 100644 index 00000000..6d4d3edd --- /dev/null +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -0,0 +1,516 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Collections; +using Serilog; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.ViewModels.Collections; + +public class WinoMailCollection +{ + // We cache each mail copy id for faster access on updates. + // If the item provider here for update or removal doesn't exist here + // we can ignore the operation. + + public HashSet MailCopyIdHashSet = []; + + public event EventHandler MailItemRemoved; + + private ListItemComparer listComparer = new(); + + private readonly ObservableGroupedCollection _mailItemSource = new ObservableGroupedCollection(); + + public ReadOnlyObservableGroupedCollection MailItems { get; } + + /// + /// Property that defines how the item sorting should be done in the collection. + /// + public SortingOptionType SortingType { get; set; } + + /// + /// Automatically deletes single mail items after the delete operation or thread->single transition. + /// This is useful when reply draft is discarded in the thread. Only enabled for Draft folder for now. + /// + public bool PruneSingleNonDraftItems { get; set; } + + public int Count => _mailItemSource.Count; + + public IDispatcher CoreDispatcher { get; set; } + + public WinoMailCollection() + { + MailItems = new ReadOnlyObservableGroupedCollection(_mailItemSource); + } + + public void Clear() => _mailItemSource.Clear(); + + private object GetGroupingKey(IMailListItem mailItem) + { + if (SortingType == SortingOptionType.ReceiveDate) + return mailItem.CreationDate.ToLocalTime().Date; + else + return mailItem.FromName; + } + + private void UpdateUniqueIdHashes(IMailHashContainer itemContainer, bool isAdd) + { + foreach (var item in itemContainer.GetContainingIds()) + { + if (isAdd) + { + MailCopyIdHashSet.Add(item); + } + else + { + MailCopyIdHashSet.Remove(item); + } + } + } + + private void InsertItemInternal(object groupKey, IMailListItem mailItem) + { + UpdateUniqueIdHashes(mailItem, true); + _mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer); + } + + private void RemoveItemInternal(ObservableGroup group, IMailListItem mailItem) + { + UpdateUniqueIdHashes(mailItem, false); + + if (mailItem is MailItemViewModel singleMailItem) + { + MailItemRemoved?.Invoke(this, singleMailItem); + } + else if (mailItem is ThreadMailItemViewModel threadViewModel) + { + foreach (var threadMailItem in threadViewModel.ThreadEmails) + { + MailItemRemoved?.Invoke(this, threadMailItem); + } + } + + group.Remove(mailItem); + + if (group.Count == 0) + { + _mailItemSource.RemoveGroup(group.Key); + } + } + + private async Task HandleThreadingAsync(ObservableGroup group, IMailListItem item, MailCopy addedItem) + { + if (item is ThreadMailItemViewModel threadViewModel) + { + await HandleExistingThreadAsync(group, threadViewModel, addedItem); + } + else if (item is MailItemViewModel mailViewModel) + { + await HandleNewThreadAsync(group, mailViewModel, addedItem); + } + } + + private async Task HandleExistingThreadAsync(ObservableGroup group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem) + { + var existingGroupKey = GetGroupingKey(threadViewModel); + + await ExecuteUIThread(() => + { + var newMailItem = new MailItemViewModel(addedItem); + threadViewModel.AddEmail(newMailItem); + }); + + var newGroupKey = GetGroupingKey(threadViewModel); + + if (!existingGroupKey.Equals(newGroupKey)) + { + await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey); + } + else + { + await ExecuteUIThread(() => { threadViewModel.NotifyPropertyChanges(); }); + } + + UpdateUniqueIdHashes(new MailItemViewModel(addedItem), true); + } + + private async Task HandleNewThreadAsync(ObservableGroup group, MailItemViewModel item, MailCopy addedItem) + { + if (item.MailCopy.UniqueId == addedItem.UniqueId) + { + await UpdateExistingItemAsync(item, addedItem); + } + else + { + await CreateNewThreadAsync(group, item, addedItem); + } + } + + private async Task MoveThreadToNewGroupAsync(ObservableGroup currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey) + { + await ExecuteUIThread(() => + { + RemoveItemInternal(currentGroup, threadViewModel); + InsertItemInternal(newGroupKey, threadViewModel); + }); + } + + private async Task CreateNewThreadAsync(ObservableGroup group, MailItemViewModel item, MailCopy addedItem) + { + var threadViewModel = new ThreadMailItemViewModel(item.MailCopy.ThreadId); + + await ExecuteUIThread(() => + { + threadViewModel.AddEmail(item); + threadViewModel.AddEmail(new MailItemViewModel(addedItem)); + }); + + var newGroupKey = GetGroupingKey(threadViewModel); + + await ExecuteUIThread(() => + { + RemoveItemInternal(group, item); + InsertItemInternal(newGroupKey, threadViewModel); + }); + } + + public async Task AddAsync(MailCopy addedItem) + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + // Compare ThreadIds - if they match and both have ThreadIds, thread them together + bool shouldThread = !string.IsNullOrEmpty(addedItem.ThreadId) && + item is MailItemViewModel mailItem && + !string.IsNullOrEmpty(mailItem.MailCopy.ThreadId) && + string.Equals(addedItem.ThreadId, mailItem.MailCopy.ThreadId, StringComparison.OrdinalIgnoreCase); + + if (!shouldThread && item is ThreadMailItemViewModel threadViewModel) + { + // Check if any email in the thread has matching ThreadId + shouldThread = !string.IsNullOrEmpty(addedItem.ThreadId) && + threadViewModel.ThreadEmails.Any(e => + !string.IsNullOrEmpty(e.MailCopy.ThreadId) && + string.Equals(addedItem.ThreadId, e.MailCopy.ThreadId, StringComparison.OrdinalIgnoreCase)); + } + + if (shouldThread) + { + await HandleThreadingAsync(group, item, addedItem); + return; + } + else if (item is MailItemViewModel itemViewModel && itemViewModel.MailCopy.UniqueId == addedItem.UniqueId) + { + await UpdateExistingItemAsync(itemViewModel, addedItem); + return; + } + } + } + + await AddNewItemAsync(addedItem); + } + + private async Task AddNewItemAsync(MailCopy addedItem) + { + var newMailItem = new MailItemViewModel(addedItem); + var groupKey = GetGroupingKey(newMailItem); + await ExecuteUIThread(() => { InsertItemInternal(groupKey, newMailItem); }); + } + + private async Task UpdateExistingItemAsync(MailItemViewModel existingItem, MailCopy updatedItem) + { + UpdateUniqueIdHashes(existingItem, false); + UpdateUniqueIdHashes(new MailItemViewModel(updatedItem), true); + + await ExecuteUIThread(() => { existingItem.MailCopy = updatedItem; }); + } + + public void AddRange(IEnumerable items, bool clearIdCache) + { + if (clearIdCache) + { + MailCopyIdHashSet.Clear(); + } + + var groupedByName = items + .GroupBy(a => GetGroupingKey(a)) + .Select(a => new ObservableGroup(a.Key, a)); + + foreach (var group in groupedByName) + { + // Store all mail copy ids for faster access. + foreach (var item in group) + { + UpdateUniqueIdHashes(item, true); + } + + var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key); + + if (existingGroup == null) + { + _mailItemSource.AddGroup(group.Key, group); + } + else + { + foreach (var item in group) + { + existingGroup.Add(item); + } + } + } + } + + public MailItemContainer GetMailItemContainer(Guid uniqueMailId) + { + var groupCount = _mailItemSource.Count; + + for (int i = 0; i < groupCount; i++) + { + var group = _mailItemSource[i]; + + for (int k = 0; k < group.Count; k++) + { + var item = group[k]; + + if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.MailCopy.UniqueId == uniqueMailId) + return new MailItemContainer(singleMailItemViewModel); + else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(uniqueMailId)) + { + var singleItemViewModel = threadMailItemViewModel.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == uniqueMailId); + + return new MailItemContainer(singleItemViewModel, threadMailItemViewModel); + } + } + } + + return null; + } + + public void UpdateThumbnails(string address) + { + if (CoreDispatcher == null) return; + + CoreDispatcher.ExecuteOnUIThread(() => + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase)) + { + mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent; + } + else if (item is ThreadMailItemViewModel threadViewModel) + { + foreach (var threadMailItem in threadViewModel.ThreadEmails) + { + if (threadMailItem.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase)) + { + threadMailItem.ThumbnailUpdatedEvent = !threadMailItem.ThumbnailUpdatedEvent; + } + } + } + } + } + }); + } + + /// + /// Fins the item container that updated mail copy belongs to and updates it. + /// + /// Updated mail copy. + /// + public async Task UpdateMailCopy(MailCopy updatedMailCopy) + { + // This item doesn't exist in the list. + if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId)) + { + return; + } + + await ExecuteUIThread(() => + { + var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId); + + if (itemContainer == null) return; + + if (itemContainer.ItemViewModel != null) + { + UpdateUniqueIdHashes(itemContainer.ItemViewModel, false); + } + + if (itemContainer.ItemViewModel != null) + { + itemContainer.ItemViewModel.MailCopy = updatedMailCopy; + } + + UpdateUniqueIdHashes(new MailItemViewModel(updatedMailCopy), true); + + // Call thread notifications if possible. + itemContainer.ThreadViewModel?.NotifyPropertyChanges(); + }); + } + + public MailItemViewModel GetNextItem(MailCopy mailCopy) + { + try + { + var groupCount = _mailItemSource.Count; + + for (int i = 0; i < groupCount; i++) + { + var group = _mailItemSource[i]; + + for (int k = 0; k < group.Count; k++) + { + var item = group[k]; + + if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.MailCopy.UniqueId == mailCopy.UniqueId) + { + if (k + 1 < group.Count) + { + return group[k + 1] as MailItemViewModel; + } + else if (i + 1 < groupCount) + { + return _mailItemSource[i + 1][0] as MailItemViewModel; + } + else + { + return null; + } + } + else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId)) + { + var singleItemViewModel = threadMailItemViewModel.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == mailCopy.UniqueId); + + if (singleItemViewModel == null) return null; + + var singleItemIndex = threadMailItemViewModel.ThreadEmails.ToList().IndexOf(singleItemViewModel); + + if (singleItemIndex + 1 < threadMailItemViewModel.ThreadEmails.Count) + { + return threadMailItemViewModel.ThreadEmails[singleItemIndex + 1]; + } + else if (i + 1 < groupCount) + { + return _mailItemSource[i + 1][0] as MailItemViewModel; + } + else + { + return null; + } + } + } + } + + return null; + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to find the next item to select."); + } + + return null; + } + + public async Task RemoveAsync(MailCopy removeItem) + { + // This item doesn't exist in the list. + if (!MailCopyIdHashSet.Contains(removeItem.UniqueId)) return; + + // Check all items for whether this item should be threaded with them. + bool shouldExit = false; + + var groupCount = _mailItemSource.Count; + + for (int i = 0; i < groupCount; i++) + { + if (shouldExit) break; + + var group = _mailItemSource[i]; + + for (int k = 0; k < group.Count; k++) + { + var item = group[k]; + + if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(removeItem.UniqueId)) + { + var removalItem = threadMailItemViewModel.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == removeItem.UniqueId); + + if (removalItem == null) return; + + // Threads' Id is equal to the last item they hold. + // We can't do Id check here because that'd remove the whole thread. + + /* Remove item from the thread. + * If thread had 1 item inside: + * -> Remove the thread and insert item as single item. + * If thread had 0 item inside: + * -> Remove the thread. + */ + + var oldGroupKey = GetGroupingKey(threadMailItemViewModel); + + await ExecuteUIThread(() => { threadMailItemViewModel.RemoveEmail(removalItem); }); + + if (threadMailItemViewModel.EmailCount == 1) + { + // Convert to single item. + + var singleViewModel = threadMailItemViewModel.ThreadEmails.First(); + var groupKey = GetGroupingKey(singleViewModel); + + await ExecuteUIThread(() => + { + RemoveItemInternal(group, threadMailItemViewModel); + InsertItemInternal(groupKey, singleViewModel); + }); + + // If thread->single conversion is being done, we should ignore it for non-draft items. + // eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added. + + if (PruneSingleNonDraftItems && !singleViewModel.IsDraft) + { + // This item should not be here anymore. + // It's basically a reply mail in Draft folder. + var newGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey); + + if (newGroup != null) + { + await ExecuteUIThread(() => { RemoveItemInternal(newGroup, singleViewModel); }); + } + } + } + else if (threadMailItemViewModel.EmailCount == 0) + { + await ExecuteUIThread(() => { RemoveItemInternal(group, threadMailItemViewModel); }); + } + else + { + // Item inside the thread is removed - update hash + UpdateUniqueIdHashes(removalItem, false); + } + + shouldExit = true; + break; + } + else if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.UniqueId == removeItem.UniqueId) + { + await ExecuteUIThread(() => { RemoveItemInternal(group, item); }); + + shouldExit = true; + + break; + } + } + } + } + + private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action); +} diff --git a/Wino.Mail.ViewModels/Data/IMailListItem.cs b/Wino.Mail.ViewModels/Data/IMailListItem.cs new file mode 100644 index 00000000..ffa84c91 --- /dev/null +++ b/Wino.Mail.ViewModels/Data/IMailListItem.cs @@ -0,0 +1,25 @@ +using System; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Mail.ViewModels.Data; + +/// +/// Common interface for mail items that can be displayed in a mail list. +/// Implemented by both MailItemViewModel and ThreadMailItemViewModel. +/// +public interface IMailListItem : IMailHashContainer +{ + /// + /// Gets the latest creation date for sorting purposes. + /// For MailItemViewModel: the mail's creation date + /// For ThreadMailItemViewModel: the latest email's creation date + /// + DateTime CreationDate { get; } + + /// + /// Gets the sender's name for grouping purposes. + /// For MailItemViewModel: the mail's from name + /// For ThreadMailItemViewModel: the latest email's from name + /// + string FromName { get; } +} diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index dc11402a..d6d37721 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -1,13 +1,17 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Interfaces; namespace Wino.Mail.ViewModels.Data; /// /// Single view model for IMailItem representation. /// -public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject +public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailListItem { + public DateTime CreationDate => MailCopy.CreationDate; [ObservableProperty] public partial MailCopy MailCopy { get; set; } = mailCopy; @@ -85,4 +89,6 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject get => MailCopy.HasAttachments; set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n); } + + public IEnumerable GetContainingIds() => [MailCopy.UniqueId]; } diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index 3ceb7f11..60151c70 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -2,13 +2,14 @@ using System.Collections.Generic; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Interfaces; namespace Wino.Mail.ViewModels.Data; /// /// Thread mail item (multiple IMailItem) view model representation. /// -public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable +public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable, IMailListItem { private readonly string _threadId; @@ -30,21 +31,21 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable /// /// Gets the latest email's subject for display /// - public string? Subject => _threadEmails + public string Subject => _threadEmails .OrderByDescending(e => e.MailCopy?.CreationDate) .FirstOrDefault()?.MailCopy?.Subject; /// /// Gets the latest email's sender name for display /// - public string? FromName => _threadEmails + public string FromName => _threadEmails .OrderByDescending(e => e.MailCopy?.CreationDate) .FirstOrDefault()?.MailCopy?.SenderContact.Name; /// /// Gets the latest email's creation date for sorting /// - public DateTime LatestEmailDate => _threadEmails + public DateTime CreationDate => _threadEmails .OrderByDescending(e => e.MailCopy?.CreationDate) .FirstOrDefault()?.MailCopy?.CreationDate ?? DateTime.MinValue; @@ -79,11 +80,11 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable GC.SuppressFinalize(this); } - private void NotifyPropertyChanges() + public void NotifyPropertyChanges() { OnPropertyChanged(nameof(Subject)); OnPropertyChanged(nameof(FromName)); - OnPropertyChanged(nameof(LatestEmailDate)); + OnPropertyChanged(nameof(CreationDate)); OnPropertyChanged(nameof(LatestMailViewModel)); } @@ -115,4 +116,6 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable /// Checks if this thread contains an email with the specified unique ID /// public bool HasUniqueId(Guid uniqueId) => _threadEmails.Any(email => email.MailCopy.UniqueId == uniqueId); + + public IEnumerable GetContainingIds() => ThreadEmails.Select(a => a.MailCopy.UniqueId); }