Files
duplicati/proprietary/Office365/SourceProvider/SourceProvider.User.cs
T

981 lines
41 KiB
C#
Raw Normal View History

2026-01-14 10:23:49 +01:00
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System.Net;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text.Json;
2026-01-14 10:23:49 +01:00
using Duplicati.Library.Interface;
namespace Duplicati.Proprietary.Office365;
partial class SourceProvider
{
internal UserEmailApiImpl UserEmailApi => new UserEmailApiImpl(_apiHelper);
internal class UserEmailApiImpl(APIHelper provider)
{
/// <summary>
/// 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).
/// </summary>
internal IAsyncEnumerable<GraphMessage> ListAllEmailsAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphMessage>();
// Consider adding $filter=receivedDateTime ge ... for incremental runs.
var url =
$"{baseUrl}/v1.0/users/{user}/messages" +
$"?$select={Uri.EscapeDataString(select)}" +
"&$orderby=receivedDateTime asc" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphMessage>(url, ct);
}
/// <summary>
/// Lists all messages in a specific mail folder.
/// </summary>
internal IAsyncEnumerable<GraphMessage> 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<GraphMessage>();
var url =
$"{baseUrl}/v1.0/users/{user}/mailFolders/{folder}/messages" +
$"?$select={Uri.EscapeDataString(select)}" +
"&$orderby=receivedDateTime asc" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphMessage>(url, ct);
}
internal async Task<GraphMailFolder> GetMailRootFolderAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphMailFolder>();
var url =
$"{baseUrl}/v1.0/users/{user}/mailFolders/msgfolderroot" +
$"?$select={Uri.EscapeDataString(select)}";
var folder = await provider.GetGraphItemAsync<GraphMailFolder>(url, ct).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(folder.Id))
throw new UserInformationException("Failed to read root mail folder.", nameof(SourceProvider));
return folder;
}
/// <summary>
/// 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.
/// </summary>
internal IAsyncEnumerable<GraphMailFolder> 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<GraphMailFolder>();
var url =
$"{baseUrl}/v1.0/users/{user}/mailFolders/{folder}/childFolders" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphMailFolder>(url, ct);
}
internal Task<Stream> 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);
2026-01-14 10:23:49 +01:00
}
internal Task<Stream> 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<GraphMessageRule> ListMessageRulesAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphMessageRule>();
var url =
$"{baseUrl}/v1.0/users/{user}/mailFolders/inbox/messageRules" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
return provider.GetAllGraphItemsAsync<GraphMessageRule>(url, ct);
}
internal Task<Stream> 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<GraphMessageRule>();
var url = $"{baseUrl}/v1.0/users/{user}/mailFolders/inbox/messageRules/{rule}?$select={Uri.EscapeDataString(select)}";
return provider.GetGraphItemAsStreamAsync(url, "application/json", ct);
}
internal Task<Stream> GetMailboxSettingsStreamAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphMailboxSettings>();
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<GraphMessageRule>(url, content, ct);
2026-01-14 10:23:49 +01:00
}
}
internal ContactsApiImpl ContactsApi => new ContactsApiImpl(_apiHelper);
internal class ContactsApiImpl(APIHelper provider)
{
/// <summary>
/// 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).
/// </summary>
internal IAsyncEnumerable<GraphContact> ListAllContactsAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphContact>();
var url =
$"{baseUrl}/v1.0/users/{user}/contacts" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphContact>(url, ct);
}
internal IAsyncEnumerable<GraphContactFolder> ListContactFoldersAsync(string userIdOrUpn, string? parentFolderId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphContactFolder>();
string url;
if (string.IsNullOrWhiteSpace(parentFolderId))
{
url = $"{baseUrl}/v1.0/users/{user}/contactFolders" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
}
else
{
var parent = Uri.EscapeDataString(parentFolderId);
url = $"{baseUrl}/v1.0/users/{user}/contactFolders/{parent}/childFolders" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
}
return provider.GetAllGraphItemsAsync<GraphContactFolder>(url, ct);
}
internal IAsyncEnumerable<GraphContact> 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<GraphContact>();
var url = $"{baseUrl}/v1.0/users/{user}/contactFolders/{folder}/contacts" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
return provider.GetAllGraphItemsAsync<GraphContact>(url, ct);
}
internal Task<Stream> 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);
}
2026-01-22 17:01:13 +01:00
internal IAsyncEnumerable<GraphRecipient> 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<GraphRecipient>();
var url =
$"{baseUrl}/v1.0/users/{user}/contacts/{group}/members" +
$"?$select={Uri.EscapeDataString(select)}" +
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
return provider.GetAllGraphItemsAsync<GraphRecipient>(url, ct);
}
internal IAsyncEnumerable<GraphRecipient> 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<GraphRecipient>();
var url =
$"{baseUrl}/v1.0/users/{user}/contactFolders/{folder}/contacts/{group}/members" +
$"?$select={Uri.EscapeDataString(select)}" +
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
return provider.GetAllGraphItemsAsync<GraphRecipient>(url, ct);
}
internal async Task<bool> 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<HttpRequestMessage> 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;
}
2026-01-14 10:23:49 +01:00
}
internal TodoApiImpl TodoApi => new TodoApiImpl(_apiHelper);
internal class TodoApiImpl(APIHelper provider)
{
internal IAsyncEnumerable<GraphTodoTaskList> ListUserTaskListsAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphTodoTaskList>();
var url =
$"{baseUrl}/v1.0/users/{user}/todo/lists" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphTodoTaskList>(url, ct);
}
internal IAsyncEnumerable<GraphTodoTask> 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<GraphTodoTask>();
var url =
$"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphTodoTask>(url, ct);
}
internal IAsyncEnumerable<GraphTodoChecklistItem> 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<GraphTodoChecklistItem>();
var url =
$"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/checklistItems" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphTodoChecklistItem>(url, ct);
}
internal IAsyncEnumerable<GraphTodoLinkedResource> 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<GraphTodoLinkedResource>();
var url =
$"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/linkedResources" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphTodoLinkedResource>(url, ct);
}
internal Task<Stream> 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<GraphTodoTask>();
var url =
$"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}" +
$"?$select={Uri.EscapeDataString(select)}";
return provider.GetGraphItemAsStreamAsync(url, "application/json", ct);
2026-01-14 10:23:49 +01:00
}
internal Task<Stream> 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<GraphTodoChecklistItem>();
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);
2026-01-14 10:23:49 +01:00
}
internal Task<Stream> 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<GraphTodoLinkedResource>();
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);
2026-01-14 10:23:49 +01:00
}
}
internal OnenoteApiImpl OnenoteApi => new OnenoteApiImpl(_apiHelper);
internal class OnenoteApiImpl(APIHelper provider)
{
internal IAsyncEnumerable<GraphNotebook> ListUserNotebooksAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphNotebook>();
var url =
$"{baseUrl}/v1.0/users/{user}/onenote/notebooks" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphNotebook>(url, ct);
}
internal IAsyncEnumerable<GraphOnenoteSectionGroup> ListNotebookSectionGroupsAsync(string notebookId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var nb = Uri.EscapeDataString(notebookId);
var select = GraphSelectBuilder.BuildSelect<GraphOnenoteSectionGroup>();
var url =
$"{baseUrl}/v1.0/onenote/notebooks/{nb}/sectionGroups" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphOnenoteSectionGroup>(url, ct);
}
internal IAsyncEnumerable<GraphOnenoteSectionGroup> ListSectionGroupSectionGroupsAsync(string sectionGroupId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var sg = Uri.EscapeDataString(sectionGroupId);
var select = GraphSelectBuilder.BuildSelect<GraphOnenoteSectionGroup>();
var url =
$"{baseUrl}/v1.0/onenote/sectionGroups/{sg}/sectionGroups" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphOnenoteSectionGroup>(url, ct);
}
internal IAsyncEnumerable<GraphOnenoteSection> ListSectionGroupSectionsAsync(string sectionGroupId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var sg = Uri.EscapeDataString(sectionGroupId);
var select = GraphSelectBuilder.BuildSelect<GraphOnenoteSection>();
var url =
$"{baseUrl}/v1.0/onenote/sectionGroups/{sg}/sections" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphOnenoteSection>(url, ct);
}
internal IAsyncEnumerable<GraphOnenotePage> ListSectionPagesAsync(string sectionId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var section = Uri.EscapeDataString(sectionId);
var select = GraphSelectBuilder.BuildSelect<GraphOnenotePage>();
var url =
$"{baseUrl}/v1.0/onenote/sections/{section}/pages" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphOnenotePage>(url, ct);
}
internal Task<Stream> 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);
2026-01-14 10:23:49 +01:00
}
}
internal OneDriveApiImpl OneDriveApi => new OneDriveApiImpl(_apiHelper);
internal class OneDriveApiImpl(APIHelper provider)
{
internal async Task<GraphDeltaResult<GraphDriveItem>> ListDriveDeltaAsync(
string driveId,
string? deltaLink,
CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var drive = Uri.EscapeDataString(driveId);
var select = GraphSelectBuilder.BuildSelect<GraphDriveItem>();
var nextUrl = string.IsNullOrWhiteSpace(deltaLink)
? $"{baseUrl}/v1.0/drives/{drive}/root/delta" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}"
: deltaLink;
2026-01-14 10:23:49 +01:00
var items = new List<GraphDriveItem>();
string? finalDeltaLink = null;
while (!string.IsNullOrWhiteSpace(nextUrl))
{
ct.ThrowIfCancellationRequested();
async Task<HttpRequestMessage> requestFactory(CancellationToken ct)
{
var req = new HttpRequestMessage(HttpMethod.Get, new Uri(nextUrl));
req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct);
req.Headers.Accept.Clear();
2026-01-22 17:01:13 +01:00
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application / json"));
2026-01-14 10:23:49 +01:00
return req;
}
using var resp = await provider.SendWithRetryShortAsync(requestFactory, ct).ConfigureAwait(false);
2026-01-14 10:23:49 +01:00
await APIHelper.EnsureOfficeApiSuccessAsync(resp, ct).ConfigureAwait(false);
var page = await APIHelper.ParseResponseJson<GraphDeltaPage<GraphDriveItem>>(resp, ct).ConfigureAwait(false)
?? new GraphDeltaPage<GraphDriveItem>();
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<GraphDriveItem>(items, finalDeltaLink);
}
internal IAsyncEnumerable<GraphDriveItem> ListDriveRootChildrenAsync(string driveId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var drive = Uri.EscapeDataString(driveId);
var select = GraphSelectBuilder.BuildSelect<GraphDriveItem>();
var url =
$"{baseUrl}/v1.0/drives/{drive}/root/children" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphDriveItem>(url, ct);
}
internal IAsyncEnumerable<GraphDrive> ListUserDrivesAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var select = GraphSelectBuilder.BuildSelect<GraphDrive>();
var url =
$"{baseUrl}/v1.0/users/{user}/drives" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
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<GraphDrive>(url, EvaluateStatusCode, ct),
ex => ex is HttpRequestException httpEx && httpEx.StatusCode == HttpStatusCode.ServiceUnavailable,
ct);
}
private async IAsyncEnumerable<T> WithTryCatch<T>(IAsyncEnumerable<T> source, Func<Exception, bool>? 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<GraphDrive> GetUserPrimaryDriveAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphDrive>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/users/{user}/drive" +
$"?$select={Uri.EscapeDataString(select)}";
var drive = await provider.GetGraphItemAsync<GraphDrive>(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<GraphDriveItem> ListDriveFolderChildrenAsync(
string driveId,
string folderItemId,
CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var drive = Uri.EscapeDataString(driveId);
var folder = Uri.EscapeDataString(folderItemId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphDriveItem>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/drives/{drive}/items/{folder}/children" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphDriveItem>(url, ct);
}
internal Task<Stream> 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<GraphPermission> GetDriveItemPermissionsAsync(
string driveId,
string itemId,
CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var drive = Uri.EscapeDataString(driveId);
var item = Uri.EscapeDataString(itemId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphPermission>();
var url =
$"{baseUrl}/v1.0/drives/{drive}/items/{item}/permissions" +
$"?$select={Uri.EscapeDataString(select)}";
return provider.GetAllGraphItemsAsync<GraphPermission>(url, ct);
2026-01-14 10:23:49 +01:00
}
2026-01-16 11:09:13 +01:00
internal Task<Stream> 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);
2026-01-16 11:09:13 +01:00
}
2026-01-14 10:23:49 +01:00
}
internal CalendarApiImpl CalendarApi => new CalendarApiImpl(_apiHelper);
internal class CalendarApiImpl(APIHelper provider)
{
internal IAsyncEnumerable<GraphCalendarGroup> ListUserCalendarGroupsAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphCalendarGroup>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/users/{user}/calendarGroups" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphCalendarGroup>(url, ct);
}
internal IAsyncEnumerable<GraphCalendar> ListUserCalendarsInCalendarGroupAsync(
string userIdOrUpn,
string calendarGroupId,
CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var group = Uri.EscapeDataString(calendarGroupId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphCalendar>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/users/{user}/calendarGroups/{group}/calendars" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphCalendar>(url, ct);
}
internal IAsyncEnumerable<GraphEvent> ListCalendarEventsAsync(string userIdOrUpn, string calendarId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var cal = Uri.EscapeDataString(calendarId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphEvent>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/users/{user}/calendars/{cal}/events" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphEvent>(url, ct);
}
internal Task<Stream> 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," +
2026-01-22 17:01:13 +01:00
"location,organizer,attendees,recurrence,seriesMasterId,type,iCalUId," +
2026-01-14 10:23:49 +01:00
"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<GraphAttachment> 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);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphAttachment>([
nameof(GraphAttachment.ContentBytes),
nameof(GraphAttachment.ContentId)
]);
var url =
$"{baseUrl}/v1.0/users/{user}/calendars/{cal}/events/{ev}/attachments" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
return provider.GetAllGraphItemsAsync<GraphAttachment>(url, ct);
}
internal Task<Stream> 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);
2026-01-14 10:23:49 +01:00
}
}
internal UserPlannerApiImpl UserPlannerApi => new UserPlannerApiImpl(_apiHelper);
internal class UserPlannerApiImpl(APIHelper provider)
{
internal IAsyncEnumerable<GraphPlannerTask> ListUserAssignedPlannerTasksAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphPlannerTask>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/users/{user}/planner/tasks" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphPlannerTask>(url, ct);
}
}
internal ChatApiImpl ChatApi => new ChatApiImpl(_apiHelper);
internal class ChatApiImpl(APIHelper provider)
{
internal IAsyncEnumerable<GraphChat> ListUserChatsAsync(string userIdOrUpn, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphChat>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/users/{user}/chats" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.CHATS_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphChat>(url, ct);
}
internal IAsyncEnumerable<GraphChatMember> ListChatMembersAsync(string chatId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var chat = Uri.EscapeDataString(chatId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphChatMember>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/chats/{chat}/members" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphChatMember>(url, ct);
}
internal IAsyncEnumerable<GraphChatMessage> ListChatMessagesAsync(string chatId, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var chat = Uri.EscapeDataString(chatId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphChatMessage>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/chats/{chat}/messages" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphChatMessage>(url, ct);
}
public IAsyncEnumerable<GraphChatHostedContent> ListChatHostedContentsAsync(
string chatId,
string messageId,
CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var chat = Uri.EscapeDataString(chatId);
var msg = Uri.EscapeDataString(messageId);
2026-01-22 17:01:13 +01:00
var select = GraphSelectBuilder.BuildSelect<GraphChatHostedContent>();
2026-01-14 10:23:49 +01:00
var url =
$"{baseUrl}/v1.0/chats/{chat}/messages/{msg}/hostedContents" +
$"?$select={Uri.EscapeDataString(select)}" +
2026-01-22 17:01:13 +01:00
$"&$top={APIHelper.GENERAL_PAGE_SIZE}";
2026-01-14 10:23:49 +01:00
return provider.GetAllGraphItemsAsync<GraphChatHostedContent>(url, ct);
}
public Task<Stream> 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);
2026-01-14 10:23:49 +01:00
}
}
}