// 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);
}
}
}