Skip to content

Instantly share code, notes, and snippets.

@machv
Last active June 30, 2026 13:30
Show Gist options
  • Select an option

  • Save machv/506c4ffd2b693505ee229a291a39bd36 to your computer and use it in GitHub Desktop.

Select an option

Save machv/506c4ffd2b693505ee229a291a39bd36 to your computer and use it in GitHub Desktop.
App only to mailbox
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
<PackageReference Include="Microsoft.Graph" Version="6.2.0" />
</ItemGroup>
</Project>
namespace AppRegExo;
/// <summary>
/// Strongly-typed configuration bound from the "ExchangeOnline" section of appsettings.json.
/// </summary>
public sealed class AppSettings
{
/// <summary>Azure AD (Entra ID) directory/tenant ID.</summary>
public string TenantId { get; set; } = string.Empty;
/// <summary>Application (client) ID of the app registration.</summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>Client secret value of the app registration.</summary>
public string ClientSecret { get; set; } = string.Empty;
/// <summary>Email address (UPN) of the shared mailbox to read.</summary>
public string Mailbox { get; set; } = string.Empty;
/// <summary>
/// When true, enables verbose Azure SDK / authentication logging to help troubleshoot
/// sign-in and token acquisition errors. Optional; defaults to false.
/// </summary>
public bool Verbose { get; set; }
/// <summary>
/// Returns a human-readable error if any required value is missing, otherwise null.
/// </summary>
public string? Validate()
{
if (string.IsNullOrWhiteSpace(TenantId)) return "TenantId is missing.";
if (string.IsNullOrWhiteSpace(ClientId)) return "ClientId is missing.";
if (string.IsNullOrWhiteSpace(ClientSecret)) return "ClientSecret is missing.";
if (string.IsNullOrWhiteSpace(Mailbox)) return "Mailbox is missing.";
return null;
}
}
{
"ExchangeOnline": {
"TenantId": "00000000-0000-0000-0000-000000000000",
"ClientId": "00000000-0000-0000-0000-000000000000",
"ClientSecret": "your-client-secret-value",
"Mailbox": "shared-mailbox@contoso.com",
"Verbose": false
}
}
using System.Diagnostics.Tracing;
using Azure.Core.Diagnostics;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;
namespace AppRegExo;
internal static class Program
{
private const string GraphDefaultScope = "https://graph.microsoft.com/.default";
private const int MessageCount = 10;
private static async Task<int> Main()
{
// Load configuration from appsettings.json (optional, copied next to the executable)
// and environment variables (e.g. ExchangeOnline__ClientSecret=...), which is handy for
// running in containers without baking secrets into the image.
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
.AddEnvironmentVariables()
.Build();
var settings = configuration.GetSection("ExchangeOnline").Get<AppSettings>() ?? new AppSettings();
var validationError = settings.Validate();
if (validationError is not null)
{
Console.Error.WriteLine($"Configuration error: {validationError}");
Console.Error.WriteLine("Fill in appsettings.json (see appsettings.sample.json for the expected shape).");
return 1;
}
// Optional verbose logging to help troubleshoot sign-in / token acquisition errors.
// Enable via "ExchangeOnline:Verbose": true in appsettings.json, or the environment
// variable ExchangeOnline__Verbose=true. The listener captures Azure Identity (and the
// underlying MSAL) diagnostics and writes them to the console for the lifetime of the run.
using AzureEventSourceListener? azureLogListener = settings.Verbose
? AzureEventSourceListener.CreateConsoleLogger(EventLevel.Verbose)
: null;
if (settings.Verbose)
{
Console.WriteLine("Verbose logging enabled — Azure Identity and Graph diagnostics will be written to the console.");
Console.WriteLine();
}
try
{
// App-only authentication using the client credentials flow.
var credentialOptions = new ClientSecretCredentialOptions();
if (settings.Verbose)
{
// Surface detailed authentication diagnostics, including account identifiers (PII)
// that are normally redacted, which makes login failures much easier to diagnose.
credentialOptions.Diagnostics.IsLoggingEnabled = true;
credentialOptions.Diagnostics.IsAccountIdentifierLoggingEnabled = true;
}
var credential = new ClientSecretCredential(
settings.TenantId, settings.ClientId, settings.ClientSecret, credentialOptions);
var graphClient = new GraphServiceClient(credential, new[] { GraphDefaultScope });
Console.WriteLine($"Reading the first {MessageCount} messages from the Inbox of '{settings.Mailbox}'...");
Console.WriteLine();
var response = await graphClient
.Users[settings.Mailbox]
.MailFolders["Inbox"]
.Messages
.GetAsync(request =>
{
request.QueryParameters.Top = MessageCount;
request.QueryParameters.Orderby = new[] { "receivedDateTime desc" };
request.QueryParameters.Select = new[] { "subject", "from", "receivedDateTime" };
});
var messages = response?.Value;
if (messages is null || messages.Count == 0)
{
Console.WriteLine("No messages found.");
return 0;
}
var index = 1;
foreach (var message in messages)
{
var received = message.ReceivedDateTime?.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "(unknown date)";
var sender = message.From?.EmailAddress?.Address ?? "(unknown sender)";
var subject = string.IsNullOrEmpty(message.Subject) ? "(no subject)" : message.Subject;
Console.WriteLine($"{index,2}. [{received}] {sender}");
Console.WriteLine($" {subject}");
index++;
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to retrieve messages from Exchange Online.");
Console.Error.WriteLine(settings.Verbose ? ex.ToString() : ex.Message);
if (!settings.Verbose)
{
Console.Error.WriteLine("Set \"ExchangeOnline:Verbose\" to true (or ExchangeOnline__Verbose=true) for detailed authentication diagnostics.");
}
return 1;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment