using MailKit; using MailKit.Net.Imap; using MailKit.Search; using MimeKit; using MaksIT.Results; using MaksIT.PostClient.Shared; namespace MaksIT.PostClient.Client; public sealed class ImapMailSession : IMailSession { private readonly Lock _gate = new(); private readonly MailSessionGate _io = new(); private readonly IMailAuthService _auth; private ImapClient? _imap; private MailboxAccount? _account; private MailAuthMaterial? _material; private readonly Dictionary _listOffset = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary> _listedUids = new(StringComparer.OrdinalIgnoreCase); private const MessageSummaryItems FlagItems = MessageSummaryItems.UniqueId | MessageSummaryItems.Flags; private const MessageSummaryItems ListItems = MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.Flags | MessageSummaryItems.InternalDate; public ImapMailSession(IMailAuthService auth) { _auth = auth; } public bool IsConnected { get { lock (_gate) return _imap is { IsConnected: true, IsAuthenticated: true }; } } public bool SupportsFolders => true; public async Task ConnectAsync( MailboxAccount account, string password, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(account); if (string.IsNullOrWhiteSpace(account.ImapHost)) return Result.BadRequest("Incoming host is required."); var resolved = await _auth.ResolveAsync(account, password, cancellationToken).ConfigureAwait(false); if (!resolved.IsSuccess || resolved.Value is null) return Result.UnprocessableEntity(string.Join(" ", resolved.Messages)); return await _io.RunAsync(async () => { await DisposeClientAsync().ConfigureAwait(false); var client = new ImapClient(); try { await client.ConnectAsync( account.ImapHost.Trim(), account.ImapPort, MailSocket.Incoming(account), cancellationToken).ConfigureAwait(false); await MailKitAuth.AuthenticateAsync(client, account, resolved.Value, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { client.Dispose(); return Result.UnprocessableEntity(MailAuthHint.Incoming(account, ex.Message)); } lock (_gate) { _imap = client; _account = account; _material = resolved.Value; } return Result.Ok(); }, cancellationToken).ConfigureAwait(false); } public Task>> ListFoldersAsync( CancellationToken cancellationToken = default) => _io.RunAsync(() => ListFoldersCoreAsync(cancellationToken), cancellationToken); private async Task>> ListFoldersCoreAsync(CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result>.UnprocessableEntity(null, "Not connected."); try { var layout = AccountLayout(); var folders = new List(); await AddFolderAsync(client.Inbox, folders, layout, cancellationToken).ConfigureAwait(false); foreach (var ns in client.PersonalNamespaces) { foreach (var folder in await ListNamespaceAsync(client, ns, cancellationToken).ConfigureAwait(false)) await AddFolderAsync(folder, folders, layout, cancellationToken).ConfigureAwait(false); } return Result>.Ok(MailFolderCatalog.Normalize(folders, layout)); } catch (Exception ex) { return Result>.UnprocessableEntity(null, ex.Message); } } public Task> ListMessagesAsync( string folder, IReadOnlySet? knownIds = null, CancellationToken cancellationToken = default, int itemBudget = 0) => _io.RunAsync(() => ListMessagesCoreAsync(folder, knownIds, cancellationToken, itemBudget), cancellationToken); private async Task> ListMessagesCoreAsync( string folder, IReadOnlySet? knownIds, CancellationToken cancellationToken, int itemBudget) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity(null, "Not connected."); if (string.IsNullOrWhiteSpace(folder)) return Result.BadRequest(null, "Folder is required."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); await OpenReadOnlyAsync(imapFolder, cancellationToken).ConfigureAwait(false); if (imapFolder.Count == 0) return Result.Ok(new MailFolderSync { Present = [] }); var extraHeaders = ImapFetchPolicy.ExtraHeaders(AccountProvider()); var known = knownIds ?? (IReadOnlySet)new HashSet(); var chunk = itemBudget > 0; if (!chunk) ClearListCursor(folder); if (known.Count > 0 && known.Count >= imapFolder.Count / 2) { var flagRows = await FetchSummariesAsync( imapFolder, FlagItems, extraHeaders: null, cancellationToken).ConfigureAwait(false); var present = IdsOf(flagRows); if (present.Count > 0) { var flags = FlagsOf(flagRows); var unknown = flagRows .Where(s => s.UniqueId.IsValid && !known.Contains(s.UniqueId.Id)) .OrderByDescending(s => s.UniqueId.Id) .Select(s => s.UniqueId) .ToList(); if (unknown.Count == 0) return Result.Ok(new MailFolderSync { Flags = flags, Present = present }); var take = unknown; var incomplete = false; if (chunk && unknown.Count > itemBudget) { take = unknown.Take(itemBudget).ToList(); incomplete = true; } var added = await FetchByUidsAsync(imapFolder, take, extraHeaders, cancellationToken) .ConfigureAwait(false); return Result.Ok(new MailFolderSync { Headers = HeadersOf(folder, added), Flags = flags, Present = incomplete ? null : present, Incomplete = incomplete }); } } if (!chunk) { var summaries = await FetchSummariesAsync( imapFolder, ListItems, extraHeaders, cancellationToken).ConfigureAwait(false); var headers = HeadersOf(folder, summaries); var ids = IdsOf(summaries); if (ids.Count == 0 && imapFolder.Count > 0) return Result.Ok(new MailFolderSync { Headers = headers, Present = null }); return Result.Ok(new MailFolderSync { Headers = headers, Present = ids }); } int offset; HashSet listed; lock (_gate) { _listOffset.TryGetValue(folder, out offset); if (!_listedUids.TryGetValue(folder, out listed!)) { listed = []; _listedUids[folder] = listed; } } var range = MailListBudget.NewestRange(imapFolder.Count, offset, itemBudget); if (range.Take <= 0) { ClearListCursor(folder); return Result.Ok(new MailFolderSync { Present = listed.ToList() }); } var chunkRows = await FetchRangeAsync( imapFolder, range.Start, range.End, ListItems, extraHeaders, cancellationToken).ConfigureAwait(false); foreach (var id in IdsOf(chunkRows)) listed.Add(id); lock (_gate) { if (range.Incomplete) _listOffset[folder] = range.NextOffset; else ClearListCursor(folder); } return Result.Ok(new MailFolderSync { Headers = HeadersOf(folder, chunkRows), Present = range.Incomplete ? null : listed.ToList(), Incomplete = range.Incomplete }); } catch (Exception ex) { return Result.UnprocessableEntity(null, ex.Message); } } private void ClearListCursor(string folder) { lock (_gate) { _listOffset.Remove(folder); _listedUids.Remove(folder); } } public Task> GetMessageAsync( string folder, uint id, CancellationToken cancellationToken = default) => _io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken), cancellationToken); private async Task> GetMessageCoreAsync( string folder, uint id, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity(null, "Not connected."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); await OpenReadOnlyAsync(imapFolder, cancellationToken).ConfigureAwait(false); var mime = await imapFolder.GetMessageAsync(new UniqueId(id), cancellationToken).ConfigureAwait(false); return Result.Ok(await MimeBody.FromMimeAsync(folder, id, mime, cancellationToken).ConfigureAwait(false)); } catch (Exception ex) { return Result.UnprocessableEntity(null, ex.Message); } } public Task> GetQuotaAsync(CancellationToken cancellationToken = default) => _io.RunAsync(() => GetQuotaCoreAsync(cancellationToken), cancellationToken); private async Task> GetQuotaCoreAsync(CancellationToken cancellationToken) { var client = RequireImap(); MailboxAccount? account; lock (_gate) account = _account; if (client is null) return Result.UnprocessableEntity(null, "Not connected."); if (!client.Capabilities.HasFlag(ImapCapabilities.Quota)) return Result.Ok(null); try { var quota = await client.Inbox.GetQuotaAsync(cancellationToken).ConfigureAwait(false); if (quota is null) return Result.Ok(null); return Result.Ok(new MailboxQuota { Label = account?.Label ?? "", Host = account?.ImapHost ?? "", UsedKb = quota.CurrentStorageSize, LimitKb = quota.StorageLimit }); } catch { return Result.Ok(null); } } public Task> AppendAsync( string folder, byte[] eml, CancellationToken cancellationToken = default) => _io.RunAsync(() => AppendCoreAsync(folder, eml, cancellationToken), cancellationToken); private async Task> AppendCoreAsync( string folder, byte[] eml, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity(null, "Not connected."); if (string.IsNullOrWhiteSpace(folder) || eml.Length == 0) return Result.BadRequest(null, "Folder and message are required."); try { await using var stream = new MemoryStream(eml, writable: false); var mime = await MimeMessage.LoadAsync(stream, cancellationToken).ConfigureAwait(false); IMailFolder? imapFolder; try { imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); } catch { var root = await ResolveCreateParentAsync(client, null, cancellationToken).ConfigureAwait(false); imapFolder = await root.CreateAsync(MailFolderPath.Leaf(folder), true, cancellationToken) .ConfigureAwait(false); } if (imapFolder is null) return Result.UnprocessableEntity(null, "Folder is not available."); await imapFolder.OpenAsync(FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false); var uid = await imapFolder.AppendAsync(new AppendRequest(mime), cancellationToken).ConfigureAwait(false); return Result.Ok(uid?.Id); } catch (Exception ex) { return Result.UnprocessableEntity(null, ex.Message); } } public async Task> SendAsync(MailSendRequest request, CancellationToken cancellationToken = default) { MailboxAccount? account; MailAuthMaterial? material; lock (_gate) { account = _account; material = _material; } if (account is null) return Result.UnprocessableEntity(null, "Not connected."); var resolved = await _auth.ResolveAsync(account, material?.Password, cancellationToken).ConfigureAwait(false); if (!resolved.IsSuccess || resolved.Value is null) return Result.UnprocessableEntity(null, string.Join(" ", resolved.Messages)); lock (_gate) _material = resolved.Value; return await SmtpSender.SendAsync(account, resolved.Value, request, cancellationToken).ConfigureAwait(false); } public Task MoveMessagesAsync( string fromFolder, IReadOnlyList ids, string toFolder, CancellationToken cancellationToken = default) => _io.RunAsync(() => MoveMessagesCoreAsync(fromFolder, ids, toFolder, cancellationToken), cancellationToken); private async Task MoveMessagesCoreAsync( string fromFolder, IReadOnlyList ids, string toFolder, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); if (string.IsNullOrWhiteSpace(fromFolder) || string.IsNullOrWhiteSpace(toFolder)) return Result.BadRequest("Folder is required."); if (fromFolder.Equals(toFolder, StringComparison.OrdinalIgnoreCase)) return Result.Ok(); var uids = Uids(ids); if (uids.Count == 0) return Result.BadRequest("Select at least one message."); try { var source = await GetMailFolderAsync(client, fromFolder, cancellationToken).ConfigureAwait(false); var dest = await GetMailFolderAsync(client, toFolder, cancellationToken).ConfigureAwait(false); await source.OpenAsync(FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false); try { if (client.Capabilities.HasFlag(ImapCapabilities.Move)) await source.MoveToAsync(uids, dest, cancellationToken).ConfigureAwait(false); else await CopyThenDeleteAsync(source, dest, uids, cancellationToken).ConfigureAwait(false); } catch (Exception) when (client.Capabilities.HasFlag(ImapCapabilities.Move)) { await CopyThenDeleteAsync(source, dest, uids, cancellationToken).ConfigureAwait(false); } return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } private static async Task CopyThenDeleteAsync( IMailFolder source, IMailFolder dest, IList uids, CancellationToken cancellationToken) { await source.CopyToAsync(uids, dest, cancellationToken).ConfigureAwait(false); await source.StoreAsync( uids, new StoreFlagsRequest(StoreAction.Add, MessageFlags.Deleted) { Silent = true }, cancellationToken).ConfigureAwait(false); await source.ExpungeAsync(uids, cancellationToken).ConfigureAwait(false); } public Task SetMessageFlagsAsync( string folder, IReadOnlyList ids, MailFlagUpdate update, CancellationToken cancellationToken = default) => _io.RunAsync(() => SetMessageFlagsCoreAsync(folder, ids, update, cancellationToken), cancellationToken); private async Task SetMessageFlagsCoreAsync( string folder, IReadOnlyList ids, MailFlagUpdate update, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(update); var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); var uids = Uids(ids); if (uids.Count == 0) return Result.BadRequest("Select at least one message."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); await imapFolder.OpenAsync(FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false); if (update.Seen == true) await StoreFlagsAsync(imapFolder, uids, StoreAction.Add, MessageFlags.Seen, cancellationToken).ConfigureAwait(false); if (update.Seen == false) await StoreFlagsAsync(imapFolder, uids, StoreAction.Remove, MessageFlags.Seen, cancellationToken).ConfigureAwait(false); if (update.Flagged == true) await StoreFlagsAsync(imapFolder, uids, StoreAction.Add, MessageFlags.Flagged, cancellationToken).ConfigureAwait(false); if (update.Flagged == false) await StoreFlagsAsync(imapFolder, uids, StoreAction.Remove, MessageFlags.Flagged, cancellationToken).ConfigureAwait(false); if (update.Deleted) await StoreFlagsAsync(imapFolder, uids, StoreAction.Add, MessageFlags.Deleted, cancellationToken).ConfigureAwait(false); if (update.Priority is not null) await StorePriorityAsync(imapFolder, uids, update.Priority, cancellationToken).ConfigureAwait(false); if (update.Deleted) await imapFolder.ExpungeAsync(uids, cancellationToken).ConfigureAwait(false); return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } public Task CreateFolderAsync( string name, string? parentFolder, CancellationToken cancellationToken = default) => _io.RunAsync(() => CreateFolderCoreAsync(name, parentFolder, cancellationToken), cancellationToken); private async Task CreateFolderCoreAsync( string name, string? parentFolder, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); var leaf = name.Trim(); if (leaf.Length == 0) return Result.BadRequest("Folder name is required."); if (leaf.IndexOfAny(['/', '\\']) >= 0) return Result.BadRequest("Folder name cannot contain slashes."); try { var parent = await ResolveCreateParentAsync(client, parentFolder, cancellationToken) .ConfigureAwait(false); await parent.CreateAsync(leaf, true, cancellationToken).ConfigureAwait(false); return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } public Task RenameFolderAsync( string folder, string? parentFolder, string name, CancellationToken cancellationToken = default) => _io.RunAsync(() => RenameFolderCoreAsync(folder, parentFolder, name, cancellationToken), cancellationToken); private async Task RenameFolderCoreAsync( string folder, string? parentFolder, string name, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); if (string.IsNullOrWhiteSpace(folder)) return Result.BadRequest("Folder is required."); if (MailFolderRole.IsSystemKind(MailFolderRole.Kind(null, folder))) return Result.BadRequest("System folders cannot be moved."); var leaf = string.IsNullOrWhiteSpace(name) ? MailFolderPath.Leaf(folder) : name.Trim(); if (leaf.Length == 0) return Result.BadRequest("Folder name is required."); if (leaf.IndexOfAny(['/', '\\']) >= 0) return Result.BadRequest("Folder name cannot contain slashes."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); if (imapFolder.IsNamespace || imapFolder.Attributes.HasFlag(FolderAttributes.Inbox)) return Result.BadRequest("System folders cannot be moved."); if (MailFolderPath.IsSelfOrUnder(parentFolder, folder)) return Result.BadRequest("A folder cannot be moved into itself."); if (imapFolder.IsOpen) await imapFolder.CloseAsync(false, cancellationToken).ConfigureAwait(false); await OpenReadOnlyAsync(client.Inbox, cancellationToken).ConfigureAwait(false); imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); if (imapFolder.IsOpen) await imapFolder.CloseAsync(false, cancellationToken).ConfigureAwait(false); var parent = await ResolveCreateParentAsync(client, parentFolder, cancellationToken) .ConfigureAwait(false); await imapFolder.RenameAsync(parent, leaf, cancellationToken).ConfigureAwait(false); return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } public Task DeleteFolderAsync(string folder, CancellationToken cancellationToken = default) => _io.RunAsync(() => DeleteFolderCoreAsync(folder, cancellationToken), cancellationToken); private async Task DeleteFolderCoreAsync(string folder, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); if (string.IsNullOrWhiteSpace(folder)) return Result.BadRequest("Folder is required."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); if (imapFolder.IsNamespace || imapFolder.Attributes.HasFlag(FolderAttributes.Inbox)) return Result.BadRequest("System folders cannot be deleted."); try { await imapFolder.StatusAsync(StatusItems.Count, cancellationToken).ConfigureAwait(false); } catch { } if (imapFolder.Count > 0) { var emptied = await EmptyFolderCoreAsync(folder, null, cancellationToken).ConfigureAwait(false); if (!emptied.IsSuccess) return emptied; imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); } if (imapFolder.IsOpen) await imapFolder.CloseAsync(false, cancellationToken).ConfigureAwait(false); await OpenReadOnlyAsync(client.Inbox, cancellationToken).ConfigureAwait(false); imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); if (imapFolder.IsOpen) await imapFolder.CloseAsync(false, cancellationToken).ConfigureAwait(false); if (imapFolder.IsSubscribed) { try { await imapFolder.UnsubscribeAsync(cancellationToken).ConfigureAwait(false); } catch { } } try { await imapFolder.DeleteAsync(cancellationToken).ConfigureAwait(false); } catch (InvalidOperationException) { if (imapFolder.IsOpen) await imapFolder.CloseAsync(false, cancellationToken).ConfigureAwait(false); await OpenReadOnlyAsync(client.Inbox, cancellationToken).ConfigureAwait(false); await imapFolder.DeleteAsync(cancellationToken).ConfigureAwait(false); } return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } public Task EmptyFolderAsync( string folder, string? trashFolder, CancellationToken cancellationToken = default) => _io.RunAsync(() => EmptyFolderCoreAsync(folder, trashFolder, cancellationToken), cancellationToken); private async Task EmptyFolderCoreAsync( string folder, string? trashFolder, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); if (string.IsNullOrWhiteSpace(folder)) return Result.BadRequest("Folder is required."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); try { await imapFolder.StatusAsync(StatusItems.Count, cancellationToken).ConfigureAwait(false); } catch { } if (imapFolder.Count == 0) return Result.Ok(); await imapFolder.OpenAsync(FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false); var uids = await imapFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); if (uids.Count == 0) return Result.Ok(); var toTrash = !string.IsNullOrWhiteSpace(trashFolder) && !folder.Equals(trashFolder, StringComparison.OrdinalIgnoreCase); if (toTrash) { var dest = await GetMailFolderAsync(client, trashFolder!, cancellationToken).ConfigureAwait(false); if (client.Capabilities.HasFlag(ImapCapabilities.Move)) await imapFolder.MoveToAsync(uids, dest, cancellationToken).ConfigureAwait(false); else { await imapFolder.CopyToAsync(uids, dest, cancellationToken).ConfigureAwait(false); await StoreFlagsAsync(imapFolder, uids.ToList(), StoreAction.Add, MessageFlags.Deleted, cancellationToken) .ConfigureAwait(false); await imapFolder.ExpungeAsync(uids, cancellationToken).ConfigureAwait(false); } } else { await StoreFlagsAsync(imapFolder, uids.ToList(), StoreAction.Add, MessageFlags.Deleted, cancellationToken) .ConfigureAwait(false); await imapFolder.ExpungeAsync(uids, cancellationToken).ConfigureAwait(false); } return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } public Task SetFolderSeenAsync( string folder, bool seen, CancellationToken cancellationToken = default) => _io.RunAsync(() => SetFolderSeenCoreAsync(folder, seen, cancellationToken), cancellationToken); private async Task SetFolderSeenCoreAsync( string folder, bool seen, CancellationToken cancellationToken) { var client = RequireImap(); if (client is null) return Result.UnprocessableEntity("Not connected."); if (string.IsNullOrWhiteSpace(folder)) return Result.BadRequest("Folder is required."); try { var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false); await imapFolder.OpenAsync(FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false); var uids = await imapFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); if (uids.Count == 0) return Result.Ok(); await StoreFlagsAsync( imapFolder, uids.ToList(), seen ? StoreAction.Add : StoreAction.Remove, MessageFlags.Seen, cancellationToken).ConfigureAwait(false); return Result.Ok(); } catch (Exception ex) { return Result.UnprocessableEntity(ex.Message); } } public async ValueTask DisposeAsync() { await _io.RunAsync(DisposeClientAsync, CancellationToken.None).ConfigureAwait(false); _io.Dispose(); } private ImapClient? RequireImap() { lock (_gate) return _imap is { IsConnected: true, IsAuthenticated: true } ? _imap : null; } private string? AccountProvider() { lock (_gate) return _account?.Provider; } private async Task DisposeClientAsync() { ImapClient? client; lock (_gate) { client = _imap; _imap = null; } if (client is null) return; try { if (client.IsConnected) await client.DisconnectAsync(true).ConfigureAwait(false); } catch { } client.Dispose(); } private static async Task GetMailFolderAsync( ImapClient client, string folder, CancellationToken cancellationToken) { if (ImapFetchPolicy.IsInboxPath(folder)) return client.Inbox; return await client.GetFolderAsync(folder, cancellationToken).ConfigureAwait(false); } private static async Task ResolveCreateParentAsync( ImapClient client, string? parentFolder, CancellationToken cancellationToken) { if (!string.IsNullOrWhiteSpace(parentFolder)) return await GetMailFolderAsync(client, parentFolder, cancellationToken).ConfigureAwait(false); if (client.PersonalNamespaces.Count > 0) return client.GetFolder(client.PersonalNamespaces[0]); return client.Inbox.ParentFolder ?? client.Inbox; } private static async Task> ListNamespaceAsync( ImapClient client, FolderNamespace ns, CancellationToken cancellationToken) { try { var all = await client.GetFoldersAsync(ns, StatusItems.Unread | StatusItems.Count, false, cancellationToken) .ConfigureAwait(false); if (all.Count > 0) return all.ToList(); } catch { } try { var subscribed = await client .GetFoldersAsync(ns, StatusItems.Unread | StatusItems.Count, true, cancellationToken) .ConfigureAwait(false); if (subscribed.Count > 0) return subscribed.ToList(); } catch { } var folders = new List(); await WalkAsync(client.GetFolder(ns), folders, cancellationToken).ConfigureAwait(false); return folders; } private static async Task WalkAsync( IMailFolder root, List folders, CancellationToken cancellationToken) { IList children; try { children = await root.GetSubfoldersAsync(false, cancellationToken).ConfigureAwait(false); } catch { return; } foreach (var child in children) { folders.Add(child); await WalkAsync(child, folders, cancellationToken).ConfigureAwait(false); } } private static async Task AddFolderAsync( IMailFolder folder, List folders, MailFolderLayout layout, CancellationToken cancellationToken) { if (ShouldSkip(folder, layout)) return; if (folders.Any(f => f.FullName.Equals(folder.FullName, StringComparison.OrdinalIgnoreCase))) return; var unread = 0; var total = 0; try { if (folder.Exists) { try { await folder.StatusAsync(StatusItems.Unread | StatusItems.Count, cancellationToken) .ConfigureAwait(false); } catch { } unread = folder.Unread; total = folder.Count; } } catch { } folders.Add(new MailFolderInfo { FullName = folder.FullName, Name = folder.Name, Unread = unread, Total = total, Kind = KindFrom(folder), Delimiter = FolderDelimiter(folder) }); } private MailFolderLayout AccountLayout() { lock (_gate) return MailFolderLayout.For(_account); } private static bool ShouldSkip(IMailFolder folder, MailFolderLayout layout) { var attrs = folder.Attributes; if (attrs.HasFlag(FolderAttributes.NonExistent)) return true; if (layout.HideVirtualFolders && (attrs.HasFlag(FolderAttributes.All) || attrs.HasFlag(FolderAttributes.Flagged) || attrs.HasFlag(FolderAttributes.Important))) return true; return layout.HideVirtualFolders && MailFolderRole.IsHidden(folder.Name, folder.FullName); } private static char FolderDelimiter(IMailFolder folder) { var sep = folder.DirectorySeparator; return sep is MailFolderPath.Slash or MailFolderPath.Dot ? sep : MailFolderPath.Slash; } private static string KindFrom(IMailFolder folder) { var attrs = folder.Attributes; if (attrs.HasFlag(FolderAttributes.Inbox)) return "inbox"; if (attrs.HasFlag(FolderAttributes.Drafts)) return "drafts"; if (attrs.HasFlag(FolderAttributes.Sent)) return "sent"; if (attrs.HasFlag(FolderAttributes.Junk)) return "junk"; if (attrs.HasFlag(FolderAttributes.Trash)) return "trash"; if (attrs.HasFlag(FolderAttributes.Archive)) return "archives"; if (attrs.HasFlag(FolderAttributes.All)) return "all"; if (attrs.HasFlag(FolderAttributes.Flagged)) return "flagged"; if (attrs.HasFlag(FolderAttributes.Important)) return "important"; return MailFolderRole.Kind(folder.Name, folder.FullName); } private static async Task OpenReadOnlyAsync(IMailFolder folder, CancellationToken cancellationToken) { if (folder.IsOpen && folder.Access != FolderAccess.ReadOnly) await folder.CloseAsync(false, cancellationToken).ConfigureAwait(false); if (!folder.IsOpen) await folder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); } private static async Task> FetchSummariesAsync( IMailFolder folder, MessageSummaryItems items, string[]? extraHeaders, CancellationToken cancellationToken) { try { var chunk = await FetchRangeAsync(folder, 0, -1, items, extraHeaders, cancellationToken) .ConfigureAwait(false); var rows = DistinctUids(chunk); if (rows.Count > 0 || folder.Count == 0) return rows; } catch { } const int batch = 500; var summaries = new List(); for (var end = folder.Count - 1; end >= 0; end -= batch) { var start = Math.Max(0, end - batch + 1); var chunk = await FetchRangeAsync(folder, start, end, items, extraHeaders, cancellationToken) .ConfigureAwait(false); summaries.AddRange(chunk); } return DistinctUids(summaries); } private static async Task> FetchByUidsAsync( IMailFolder folder, IList uids, string[]? extraHeaders, CancellationToken cancellationToken) { const int batch = 250; var summaries = new List(uids.Count); for (var start = 0; start < uids.Count; start += batch) { var slice = uids.Skip(start).Take(batch).ToList(); summaries.AddRange(await FetchSliceAsync(folder, slice, extraHeaders, cancellationToken).ConfigureAwait(false)); } return DistinctUids(summaries); } private static async Task> FetchSliceAsync( IMailFolder folder, IList uids, string[]? extraHeaders, CancellationToken cancellationToken) { if (extraHeaders is { Length: > 0 }) { try { var rows = await folder.FetchAsync(uids, new FetchRequest(ListItems, extraHeaders), cancellationToken) .ConfigureAwait(false); if (rows.Count > 0) return rows; } catch { } } return await folder.FetchAsync(uids, ListItems, cancellationToken).ConfigureAwait(false); } private static async Task> FetchRangeAsync( IMailFolder folder, int start, int end, MessageSummaryItems items, string[]? extraHeaders, CancellationToken cancellationToken) { if (extraHeaders is { Length: > 0 }) { try { var rows = await folder.FetchAsync(start, end, new FetchRequest(items, extraHeaders), cancellationToken) .ConfigureAwait(false); if (rows.Count > 0) return rows; } catch { } } return await folder.FetchAsync(start, end, items, cancellationToken).ConfigureAwait(false); } private static List DistinctUids(IEnumerable summaries) { var map = new Dictionary(); foreach (var summary in summaries) { if (!summary.UniqueId.IsValid) continue; if (map.TryGetValue(summary.UniqueId.Id, out var current) && !ImapFetchPolicy.PreferComplete( summary.Envelope is not null, current.Envelope is not null, summary.InternalDate.HasValue || summary.Envelope?.Date is not null, current.InternalDate.HasValue || current.Envelope?.Date is not null)) continue; map[summary.UniqueId.Id] = summary; } return map.Values.ToList(); } private static List HeadersOf(string folder, IEnumerable summaries) => summaries .Where(s => s.UniqueId.IsValid) .OrderByDescending(s => s.Envelope?.Date ?? s.InternalDate ?? DateTimeOffset.MinValue) .Select(s => ToHeader(folder, s)) .ToList(); private static List IdsOf(IEnumerable summaries) => summaries.Where(s => s.UniqueId.IsValid).Select(s => s.UniqueId.Id).Distinct().ToList(); private static List FlagsOf(IEnumerable summaries) => summaries .Where(s => s.UniqueId.IsValid) .Select(s => new MailFlagState { Id = s.UniqueId.Id, IsSeen = s.Flags?.HasFlag(MessageFlags.Seen) == true, IsFlagged = s.Flags?.HasFlag(MessageFlags.Flagged) == true }) .ToList(); private static MailMessageHeader ToHeader(string folder, IMessageSummary summary) { var envelope = summary.Envelope; var names = new List(); CollectPartNames(summary.Body, names); var extra = new List>(); if (summary.Headers is not null) { foreach (var header in summary.Headers) extra.Add(new KeyValuePair(header.Field, header.Value)); } var info = EnvelopeClassifier.Classify(extra, names); var keywords = summary.Keywords; var xPriority = HeaderValue(summary, "X-Priority"); var importance = HeaderValue(summary, "Importance"); var priorityHeader = HeaderValue(summary, "Priority"); return MimeBody.ToListHeader( folder, summary.UniqueId.Id, envelope?.Subject ?? "", envelope?.From?.ToString() ?? "", envelope?.Date ?? summary.InternalDate ?? DateTimeOffset.MinValue, summary.Flags?.HasFlag(MessageFlags.Seen) == true, summary.Flags?.HasFlag(MessageFlags.Flagged) == true, summary.Attachments.Any() || names.Any(EnvelopeClassifier.IsPostacertName), info, MailPriority.Resolve(keywords, xPriority, importance, priorityHeader), envelope?.MessageId, MailId.Parent( FirstNonEmpty( HeaderValue(summary, "X-Riferimento-Message-ID"), envelope?.InReplyTo, HeaderValue(summary, "In-Reply-To")), HeaderValue(summary, "References"))); } private static string FirstNonEmpty(params string?[] values) { foreach (var value in values) { if (!string.IsNullOrWhiteSpace(value)) return value.Trim(); } return ""; } private static string HeaderValue(IMessageSummary summary, string name) { if (summary.Headers is null) return ""; foreach (var header in summary.Headers) { if (header.Field.Equals(name, StringComparison.OrdinalIgnoreCase)) return header.Value ?? ""; } return ""; } private static IList Uids(IReadOnlyList ids) => ids.Where(id => id > 0).Select(id => new UniqueId(id)).Distinct().ToList(); private static async Task StorePriorityAsync( IMailFolder folder, IList uids, string priority, CancellationToken cancellationToken) { var remove = new StoreFlagsRequest(StoreAction.Remove, new[] { MailPriority.KeywordHigh, MailPriority.KeywordLow }) { Silent = true }; await folder.StoreAsync(uids, remove, cancellationToken).ConfigureAwait(false); var add = MailPriority.KeywordsFor(priority); if (add.Count == 0) return; var request = new StoreFlagsRequest(StoreAction.Add, add) { Silent = true }; await folder.StoreAsync(uids, request, cancellationToken).ConfigureAwait(false); } private static Task StoreFlagsAsync( IMailFolder folder, IList uids, StoreAction action, MessageFlags flags, CancellationToken cancellationToken) => folder.StoreAsync(uids, new StoreFlagsRequest(action, flags) { Silent = true }, cancellationToken); private static void CollectPartNames(BodyPart? part, List names) { if (part is null) return; if (part is BodyPartBasic basic && !string.IsNullOrWhiteSpace(basic.FileName)) names.Add(basic.FileName); if (part is BodyPartMultipart multi) { foreach (var child in multi.BodyParts) CollectPartNames(child, names); } } }