// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System.Net; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text.Json; using Duplicati.Library.Interface; namespace Duplicati.Proprietary.Office365; partial class SourceProvider { internal UserEmailApiImpl UserEmailApi => new UserEmailApiImpl(_apiHelper); internal class UserEmailApiImpl(APIHelper provider) { /// /// Lists all messages in the user's mailbox (across all folders) using paging via @odata.nextLink. /// Requires Microsoft Graph application permission Mail.Read (or Mail.ReadBasic.All for limited fields). /// internal IAsyncEnumerable ListAllEmailsAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); // Consider adding $filter=receivedDateTime ge ... for incremental runs. var url = $"{baseUrl}/v1.0/users/{user}/messages" + $"?$select={Uri.EscapeDataString(select)}" + "&$orderby=receivedDateTime asc" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } /// /// Lists all messages in a specific mail folder. /// internal IAsyncEnumerable ListAllEmailsInFolderAsync( string userIdOrUpn, string folderId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var folder = Uri.EscapeDataString(folderId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/{folder}/messages" + $"?$select={Uri.EscapeDataString(select)}" + "&$orderby=receivedDateTime asc" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal async Task GetMailRootFolderAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/msgfolderroot" + $"?$select={Uri.EscapeDataString(select)}"; var folder = await provider.GetGraphItemAsync(url, ct).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(folder.Id)) throw new UserInformationException("Failed to read root mail folder.", nameof(SourceProvider)); return folder; } /// /// Enumerates mail folders under a given folder. Use folderId = "msgfolderroot" to list top-level folders. /// For a full tree, call this recursively for each returned folder. /// internal IAsyncEnumerable ListMailChildFoldersAsync(string userIdOrUpn, string folderId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var folder = Uri.EscapeDataString(folderId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/{folder}/childFolders" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetEmailContentStreamAsync(string userIdOrUpn, string messageId, CancellationToken ct) { // MIME (RFC 822) content of the message // Graph returns message/rfc822 var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var msg = Uri.EscapeDataString(messageId); var url = $"{baseUrl}/v1.0/users/{user}/messages/{msg}/$value"; return provider.GetGraphResponseAsRealStreamAsync(url, "message/rfc822", ct); } internal Task GetEmailMetadataStreamAsync(string userIdOrUpn, string messageId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var msg = Uri.EscapeDataString(messageId); // Mailbox-specific state that is NOT preserved in RFC822 var select = "id,parentFolderId,isRead,importance,categories,flag," + "conversationId,conversationIndex,internetMessageId," + "receivedDateTime,sentDateTime,createdDateTime,lastModifiedDateTime," + "from,toRecipients,ccRecipients,bccRecipients,replyTo,sender,subject,hasAttachments"; var url = $"{baseUrl}/v1.0/users/{user}/messages/{msg}?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } internal IAsyncEnumerable ListMessageRulesAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/inbox/messageRules" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetMessageRuleStreamAsync(string userIdOrUpn, string ruleId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var rule = Uri.EscapeDataString(ruleId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/inbox/messageRules/{rule}?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } internal Task GetMailboxSettingsStreamAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/mailboxSettings?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } internal async Task UpdateMailboxSettingsAsync(string userIdOrUpn, GraphMailboxSettings settings, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var url = $"{baseUrl}/v1.0/users/{user}/mailboxSettings"; var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); await provider.PatchGraphItemAsync(url, content, ct); } internal async Task CreateMessageRuleAsync(string userIdOrUpn, GraphMessageRule rule, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/inbox/messageRules"; // Remove ID and read-only properties before creating rule.Id = ""; rule.IsReadOnly = null; rule.HasError = null; var json = JsonSerializer.Serialize(rule, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); await provider.PostGraphItemAsync(url, content, ct); } } internal ContactsApiImpl ContactsApi => new ContactsApiImpl(_apiHelper); internal class ContactsApiImpl(APIHelper provider) { /// /// Enumerates all contacts in the user's default Contacts container. /// For contact folders too, enumerate /contactFolders and then /contactFolders/{id}/contacts. /// Requires Microsoft Graph application permission Contacts.Read (or Contacts.ReadWrite). /// internal IAsyncEnumerable ListAllContactsAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/contacts" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListContactFoldersAsync(string userIdOrUpn, string? parentFolderId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); string url; if (string.IsNullOrWhiteSpace(parentFolderId)) { url = $"{baseUrl}/v1.0/users/{user}/contactFolders" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; } else { var parent = Uri.EscapeDataString(parentFolderId); url = $"{baseUrl}/v1.0/users/{user}/contactFolders/{parent}/childFolders" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; } return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListContactsInFolderAsync(string userIdOrUpn, string folderId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var folder = Uri.EscapeDataString(folderId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/contactFolders/{folder}/contacts" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetContactPhotoStreamAsync(string userIdOrUpn, string contactId, string? folderId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var contact = Uri.EscapeDataString(contactId); string url; if (string.IsNullOrWhiteSpace(folderId)) { url = $"{baseUrl}/v1.0/users/{user}/contacts/{contact}/photo/$value"; } else { var folder = Uri.EscapeDataString(folderId); url = $"{baseUrl}/v1.0/users/{user}/contactFolders/{folder}/contacts/{contact}/photo/$value"; } return provider.GetGraphResponseAsRealStreamAsync(url, "application/octet-stream", ct); } internal IAsyncEnumerable ListContactGroupMembersAsync( string userIdOrUpn, string contactGroupId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var group = Uri.EscapeDataString(contactGroupId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/contacts/{group}/members" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListContactGroupMembersAsync( string userIdOrUpn, string contactFolderId, string contactGroupId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var folder = Uri.EscapeDataString(contactFolderId); var group = Uri.EscapeDataString(contactGroupId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/contactFolders/{folder}/contacts/{group}/members" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal async Task IsContactGroupAsync(string userIdOrUpn, string contactId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var id = Uri.EscapeDataString(contactId); var url = $"{baseUrl}/v1.0/users/{user}/contacts/{id}/members?$top=1"; async Task requestFactory(CancellationToken rct) { var req = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, rct).ConfigureAwait(false); return req; } using var resp = await provider.SendWithRetryShortAsync(requestFactory, ct).ConfigureAwait(false); if (resp.IsSuccessStatusCode) return true; // Contacts will typically fail with 400/404 for the 'members' segment. if (resp.StatusCode == HttpStatusCode.BadRequest || resp.StatusCode == HttpStatusCode.NotFound) return false; await APIHelper.EnsureOfficeApiSuccessAsync(resp, ct).ConfigureAwait(false); return false; } } internal TodoApiImpl TodoApi => new TodoApiImpl(_apiHelper); internal class TodoApiImpl(APIHelper provider) { internal IAsyncEnumerable ListUserTaskListsAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListTaskListTasksAsync(string userIdOrUpn, string taskListId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var list = Uri.EscapeDataString(taskListId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListTaskChecklistItemsAsync( string userIdOrUpn, string taskListId, string taskId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var list = Uri.EscapeDataString(taskListId); var task = Uri.EscapeDataString(taskId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/checklistItems" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListTaskLinkedResourcesAsync( string userIdOrUpn, string taskListId, string taskId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var list = Uri.EscapeDataString(taskListId); var task = Uri.EscapeDataString(taskId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/linkedResources" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetTaskStreamAsync( string userIdOrUpn, string taskListId, string taskId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var list = Uri.EscapeDataString(taskListId); var task = Uri.EscapeDataString(taskId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}" + $"?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } internal Task GetTaskChecklistItemStreamAsync( string userIdOrUpn, string taskListId, string taskId, string checklistItemId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var list = Uri.EscapeDataString(taskListId); var task = Uri.EscapeDataString(taskId); var item = Uri.EscapeDataString(checklistItemId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/checklistItems/{item}" + $"?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } internal Task GetTaskLinkedResourceStreamAsync( string userIdOrUpn, string taskListId, string taskId, string linkedResourceId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var list = Uri.EscapeDataString(taskListId); var task = Uri.EscapeDataString(taskId); var lr = Uri.EscapeDataString(linkedResourceId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/linkedResources/{lr}" + $"?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } } internal OnenoteApiImpl OnenoteApi => new OnenoteApiImpl(_apiHelper); internal class OnenoteApiImpl(APIHelper provider) { internal IAsyncEnumerable ListUserNotebooksAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/onenote/notebooks" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListNotebookSectionGroupsAsync(string notebookId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var nb = Uri.EscapeDataString(notebookId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/onenote/notebooks/{nb}/sectionGroups" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListSectionGroupSectionGroupsAsync(string sectionGroupId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var sg = Uri.EscapeDataString(sectionGroupId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/onenote/sectionGroups/{sg}/sectionGroups" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListSectionGroupSectionsAsync(string sectionGroupId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var sg = Uri.EscapeDataString(sectionGroupId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/onenote/sectionGroups/{sg}/sections" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListSectionPagesAsync(string sectionId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var section = Uri.EscapeDataString(sectionId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/onenote/sections/{section}/pages" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetOnenotePageContentStreamAsync(string contentUrl, CancellationToken ct) { // contentUrl is typically an absolute Microsoft Graph URL. Use it as-is. // Page content is HTML; Accept header is optional but helps. return provider.GetGraphResponseAsRealStreamAsync(contentUrl, "text/html", ct); } } internal OneDriveApiImpl OneDriveApi => new OneDriveApiImpl(_apiHelper); internal class OneDriveApiImpl(APIHelper provider) { internal async Task> ListDriveDeltaAsync( string driveId, string? deltaLink, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var drive = Uri.EscapeDataString(driveId); var select = GraphSelectBuilder.BuildSelect(); var nextUrl = string.IsNullOrWhiteSpace(deltaLink) ? $"{baseUrl}/v1.0/drives/{drive}/root/delta" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}" : deltaLink; var items = new List(); string? finalDeltaLink = null; while (!string.IsNullOrWhiteSpace(nextUrl)) { ct.ThrowIfCancellationRequested(); async Task requestFactory(CancellationToken ct) { var req = new HttpRequestMessage(HttpMethod.Get, new Uri(nextUrl)); req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct); req.Headers.Accept.Clear(); req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application / json")); return req; } using var resp = await provider.SendWithRetryShortAsync(requestFactory, ct).ConfigureAwait(false); await APIHelper.EnsureOfficeApiSuccessAsync(resp, ct).ConfigureAwait(false); var page = await APIHelper.ParseResponseJson>(resp, ct).ConfigureAwait(false) ?? new GraphDeltaPage(); if (page.Value.Count > 0) items.AddRange(page.Value); if (!string.IsNullOrWhiteSpace(page.DeltaLink)) finalDeltaLink = page.DeltaLink; nextUrl = page.NextLink; } if (string.IsNullOrWhiteSpace(finalDeltaLink)) throw new UserInformationException("Drive delta did not return an @odata.deltaLink.", nameof(SourceProvider)); return new GraphDeltaResult(items, finalDeltaLink); } internal IAsyncEnumerable ListDriveRootChildrenAsync(string driveId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var drive = Uri.EscapeDataString(driveId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/drives/{drive}/root/children" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListUserDrivesAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/drives" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; bool? EvaluateStatusCode(HttpResponseMessage m) { return m.StatusCode switch { HttpStatusCode.ServiceUnavailable => false, _ => null, }; } // Map 503 to success, as it means the user has no OneDrive provisioned. return WithTryCatch( provider.GetAllGraphItemsAsync(url, EvaluateStatusCode, ct), ex => ex is HttpRequestException httpEx && httpEx.StatusCode == HttpStatusCode.ServiceUnavailable, ct); } private async IAsyncEnumerable WithTryCatch(IAsyncEnumerable source, Func? ignoreException, [EnumeratorCancellation] CancellationToken cancellationToken) { var enumerator = source.GetAsyncEnumerator(cancellationToken); while (true) { try { if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) yield break; } catch (Exception ex) { if (ignoreException?.Invoke(ex) != true) throw; yield break; } yield return enumerator.Current; } } internal async Task GetUserPrimaryDriveAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/drive" + $"?$select={Uri.EscapeDataString(select)}"; var drive = await provider.GetGraphItemAsync(url, ct).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(drive.Id)) throw new UserInformationException("Failed to read user's primary drive.", nameof(SourceProvider)); return drive; } internal IAsyncEnumerable ListDriveFolderChildrenAsync( string driveId, string folderItemId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var drive = Uri.EscapeDataString(driveId); var folder = Uri.EscapeDataString(folderItemId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/drives/{drive}/items/{folder}/children" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetDriveItemContentStreamAsync(string driveId, string itemId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var drive = Uri.EscapeDataString(driveId); var item = Uri.EscapeDataString(itemId); // Returns the file content stream (302 redirect handled by HttpClient by default) var url = $"{baseUrl}/v1.0/drives/{drive}/items/{item}/content"; return provider.GetGraphResponseAsRealStreamAsync(url, "application/octet-stream", ct); } internal IAsyncEnumerable GetDriveItemPermissionsAsync( string driveId, string itemId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var drive = Uri.EscapeDataString(driveId); var item = Uri.EscapeDataString(itemId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/drives/{drive}/items/{item}/permissions" + $"?$select={Uri.EscapeDataString(select)}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetDriveItemMetadataStreamAsync( string driveId, string itemId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var drive = Uri.EscapeDataString(driveId); var item = Uri.EscapeDataString(itemId); var select = "id,name,parentReference,webUrl,eTag,cTag,size," + "createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy," + "fileSystemInfo,file,folder,package,shared,deleted"; var url = $"{baseUrl}/v1.0/drives/{drive}/items/{item}?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } } internal CalendarApiImpl CalendarApi => new CalendarApiImpl(_apiHelper); internal class CalendarApiImpl(APIHelper provider) { internal IAsyncEnumerable ListUserCalendarGroupsAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/calendarGroups" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListUserCalendarsInCalendarGroupAsync( string userIdOrUpn, string calendarGroupId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var group = Uri.EscapeDataString(calendarGroupId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/calendarGroups/{group}/calendars" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListCalendarEventsAsync(string userIdOrUpn, string calendarId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var cal = Uri.EscapeDataString(calendarId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/calendars/{cal}/events" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetCalendarEventStreamAsync( string userIdOrUpn, string calendarId, string eventId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var cal = Uri.EscapeDataString(calendarId); var ev = Uri.EscapeDataString(eventId); var select = "id,subject,createdDateTime,lastModifiedDateTime,start,end,isAllDay,isCancelled," + "location,organizer,attendees,recurrence,seriesMasterId,type,iCalUId," + "body,bodyPreview,importance,sensitivity,categories,showAs,responseStatus,onlineMeeting,webLink"; var url = $"{baseUrl}/v1.0/users/{user}/calendars/{cal}/events/{ev}" + $"?$select={Uri.EscapeDataString(select)}"; return provider.GetGraphItemAsStreamAsync(url, "application/json", ct); } internal IAsyncEnumerable ListCalendarEventAttachmentsAsync( string userIdOrUpn, string calendarId, string eventId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var cal = Uri.EscapeDataString(calendarId); var ev = Uri.EscapeDataString(eventId); var select = GraphSelectBuilder.BuildSelect([ nameof(GraphAttachment.ContentBytes), nameof(GraphAttachment.ContentId) ]); var url = $"{baseUrl}/v1.0/users/{user}/calendars/{cal}/events/{ev}/attachments" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal Task GetCalendarEventAttachmentStreamAsync( string userIdOrUpn, string calendarId, string eventId, string attachmentId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var cal = Uri.EscapeDataString(calendarId); var ev = Uri.EscapeDataString(eventId); var att = Uri.EscapeDataString(attachmentId); var url = $"{baseUrl}/v1.0/users/{user}/calendars/{cal}/events/{ev}/attachments/{att}/$value"; return provider.GetGraphResponseAsRealStreamAsync(url, "application/octet-stream", ct); } } internal UserPlannerApiImpl UserPlannerApi => new UserPlannerApiImpl(_apiHelper); internal class UserPlannerApiImpl(APIHelper provider) { internal IAsyncEnumerable ListUserAssignedPlannerTasksAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/planner/tasks" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } } internal ChatApiImpl ChatApi => new ChatApiImpl(_apiHelper); internal class ChatApiImpl(APIHelper provider) { internal IAsyncEnumerable ListUserChatsAsync(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/users/{user}/chats" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.CHATS_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListChatMembersAsync(string chatId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var chat = Uri.EscapeDataString(chatId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/chats/{chat}/members" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable ListChatMessagesAsync(string chatId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var chat = Uri.EscapeDataString(chatId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/chats/{chat}/messages" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } public IAsyncEnumerable ListChatHostedContentsAsync( string chatId, string messageId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var chat = Uri.EscapeDataString(chatId); var msg = Uri.EscapeDataString(messageId); var select = GraphSelectBuilder.BuildSelect(); var url = $"{baseUrl}/v1.0/chats/{chat}/messages/{msg}/hostedContents" + $"?$select={Uri.EscapeDataString(select)}" + $"&$top={APIHelper.GENERAL_PAGE_SIZE}"; return provider.GetAllGraphItemsAsync(url, ct); } public Task GetChatHostedContentValueStreamAsync( string chatId, string messageId, string hostedContentId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var chat = Uri.EscapeDataString(chatId); var msg = Uri.EscapeDataString(messageId); var hc = Uri.EscapeDataString(hostedContentId); var url = $"{baseUrl}/v1.0/chats/{chat}/messages/{msg}/hostedContents/{hc}/$value"; return provider.GetGraphItemAsStreamAsync(url, "application/octet-stream", ct); } } }