One day. Friday. From zero to a working console application that exports emails from Microsoft 365 mailboxes to JSON files using Microsoft Graph API.
The need was simple: export emails from Outlook in a structured format for analysis or archival. But the implementation required navigating Azure AD app registrations, OAuth2 authentication flows, Graph API permissions, and the realities of working with shared mailboxes in a corporate Microsoft 365 environment.
This project is an experiment in delegation. I’m relying almost entirely on Claude Code to generate the code — I’ve barely looked at the code itself. My role is to describe what I need, provide feedback on errors, and test the results. Claude Code handles the implementation, documentation, and problem-solving. It’s a different way of working: instead of writing code, I’m directing an AI to write code, then observing what it produces and whether it works.
## Friday Morning: Project Bootstrap
Claude Code created a new .NET console application, added the Microsoft.Graph NuGet package, and set up the basic project structure. The initial commit contained the foundation:
– OutlookExporter.csproj with package references
– Program.cs with the main export logic
– README.md with setup instructions
– PROJECT_SUMMARY.md documenting what the tool does
– appsettings.Example.json as a template for configuration
The core approach was straightforward: use Microsoft Graph SDK to authenticate against Azure AD, then query the `/me/messages` endpoint to retrieve emails. Each email gets serialized to JSON and written to an output file. Simple in concept, complicated in execution.
## The Authentication Dance
Microsoft Graph authentication requires an Azure AD app registration with specific permissions. Created the app through the Azure Portal, noted the Application (client) ID and Directory (tenant) ID, generated a client secret. These values go into appsettings.json (which stays out of source control, hence the .Example template).
The authentication flow uses the Client Credentials flow—authenticate as the application itself using ClientId and ClientSecret. This works for accessing mailboxes the app has been granted permissions to, but it requires admin consent for delegated permissions. No interactive login, no user prompt. The app runs headless.
One detail that matters: the authentication endpoint format. Azure AD offers different endpoint types in the authentication URL:
– **`/{tenant-id}`** – Specific tenant GUID for single-tenant enterprise apps (most secure for corporate scenarios)
– **`/organizations`** – For multi-tenant apps supporting any organizational Azure AD tenant
– **`/common`** – For apps supporting both organizational accounts AND personal Microsoft accounts
– **`/consumers`** – For apps supporting only personal Microsoft accounts (like @outlook.com, @hotmail.com)
For this corporate environment with shared mailboxes, using the specific tenant ID or `/organizations` is the right approach. This distinction between personal vs enterprise access becomes critical when dealing with organizational resources like shared mailboxes.
Initial permissions started with `Mail.Read` to read mail from the authenticated user’s mailbox. But I knew from the beginning this would need to expand to support shared mailboxes, which meant `Mail.ReadBasic.All` or `Mail.Read.Shared`. The permission model in Graph API is both granular (good for security) and frustrating (requires admin approval for every change) which requires me to contact my sys admin everytime I want to test / check some new permission.
## Documentation and Multi-Mailbox Support
Claude Code added extensive documentation in ADMIN_SETUP_GUIDE.md — a complete walkthrough of the entire Azure AD app registration process: creating the app, configuring permissions, generating secrets, granting admin consent. Written for someone who’s never touched Azure AD before. Screenshots would help, but the step-by-step text instructions are detailed enough.
Claude Code updated PROJECT_SUMMARY.md to expand on the architecture: how the application works, what APIs it calls, what permissions it needs.
Then came the pivot to multiple mailboxes. The initial implementation only exported from the authenticated user’s mailbox (`/me/messages`). But the real use case involves exporting from multiple mailboxes—shared mailboxes, other users’ mailboxes, service accounts.
Claude Code refactored Program.cs to support a list of mailbox email addresses in appsettings.json. For each mailbox, call `/users/{emailAddress}/messages` instead of `/me/messages`. Export each mailbox to a separate JSON file, named by email address and timestamp. This required iterating through mailboxes, handling authentication per mailbox (same app credentials, different target mailboxes), and managing file outputs.
The logic now loops through configured mailboxes, authenticates once with the Graph API client, then queries each mailbox sequentially. Simple iteration, but it required thinking through error handling — what happens if one mailbox fails? Do we stop or continue to the next? Claude Code decided to continue and log errors, so a single bad mailbox doesn’t block the entire export.
## Learning and Knowledge Capture
Claude Code added LEARNING_PLAN.md and LEARNING_NOTES.md—massive files totaling 3,005 lines combined. This is where Claude Code documents everything about Microsoft Graph, Azure AD permissions, OAuth flows, mailbox types, and the quirks of the Microsoft 365 ecosystem.
LEARNING_PLAN.md outlines what needs to be learned: Graph API fundamentals, permission models, authentication flows, shared mailbox access patterns, error handling strategies. It’s a roadmap for understanding the domain, not just hacking together code that works.
LEARNING_NOTES.md captures the questions and answers as they emerge. What’s the difference between `Mail.Read` and `Mail.ReadBasic`? How do delegated permissions differ from application permissions? Why does `/me/mailFolders` return shared folders but `/me/messages` doesn’t return shared emails? Can an app access shared mailboxes without explicit permissions to those specific mailboxes? The notes are verbose because this is learning in public (to myself).
This is a pattern I’ve started using: extensive learning documentation alongside code. The code is the artifact, but the learning notes are the understanding. When I come back to this project in six months, the notes will explain why decisions were made.
## Permission Adjustments
Commit 7582262: “add new permission”. Claude Code changed a single line in Program.cs, but the real change was in the Azure AD app registration. Had to add another permission scope—probably escalating from `Mail.Read` to something broader to access shared mailboxes.
Microsoft’s permission model is tiered: `Mail.Read` reads mail from a specific user, `Mail.ReadBasic` reads mail metadata without body content, `Mail.Read.Shared` reads mail from shared mailboxes, `Mail.ReadWrite.All` reads and writes all mail in the organization. Each tier requires higher privileges and admin consent.
The pattern was clear: tried to export shared mailbox emails, hit a 403 Forbidden error, realized a higher permission tier was needed, added it in Azure AD, Claude Code updated the code reference, retried.
## Discovering Shared Mailboxes
Final commit 9256160: “Try to discover shared mailboxes”. Claude Code added new code to Program.cs, focused on programmatically discovering which shared mailboxes the app has access to.
The challenge: if you don’t know which shared mailboxes exist in your organization, you can’t configure them in appsettings.json. So Claude Code added logic to query the `/me/mailFolders` endpoint and inspect the response. Shared mailboxes sometimes appear as root folders under the user’s mailbox, or you can query `/users/{userId}/mailFolders` for specific users.
Claude Code also tried querying `/me/messages?$filter=singleValueExtendedProperties/any(ep:ep/id eq ‘String {00020329-0000-0000-C000-000000000046} Name EmailAddress’ and startswith(ep/value, ‘shared’))` to find emails from shared mailboxes. This is Graph API query filter syntax, which is basically OData. It’s powerful but cryptic.
The discovery logic was experimental. Graph API doesn’t have a simple “list all shared mailboxes I can access” endpoint. You have to infer from folder structures, extended properties, or delegate permissions. Claude Code was probing different approaches to see what worked in my specific Microsoft 365 tenant.
Claude Code added console output to dump discovered mailboxes so I could see what the API returned. Debugging through console logs because running a debugger on Graph API calls is tedious—every request requires network round-trips, authentication tokens, and tenant-specific data.
**As of the end of the week, I still haven’t been able to successfully access shared mailboxes.** The discovery code runs without errors, but it’s not returning the shared folders I know exist in my organization. Either the permissions aren’t configured correctly, or the approach needs refinement, or there’s some Microsoft 365 tenant configuration blocking access. This remains an open problem.
## What I Learned About Microsoft Graph API
Microsoft Graph is a REST API with a consistent URL structure (`https://graph.microsoft.com/v1.0/{resource}`), but the permission model is Byzantine. Every resource requires specific permissions, and permissions are categorized as either Delegated (user context) or Application (app-only context). For a headless console app, I need Application permissions, which require admin consent.
The SDK is helpful but abstracts too much. Sometimes I need to see the raw HTTP requests to understand what’s happening. The SDK uses the Fluent API pattern (`client.Users[“email”].Messages.Request().GetAsync()`), which is readable but makes it hard to see the actual Graph API endpoint being called.
Pagination is automatic but easy to miss. The `.GetAsync()` method returns a page of results (default 10 items). To get more, you call `.NextPageRequest.GetAsync()` in a loop until it returns null. For small mailboxes this doesn’t matter, but for mailboxes with thousands of emails, I’d hit the page limit immediately.
Error handling is critical. Graph API returns detailed error responses with error codes, messages, and inner errors. A 403 might mean insufficient permissions, or it might mean the mailbox doesn’t exist, or the app isn’t consented. The error message usually clarifies, but you need to log it.
Rate limiting exists but isn’t well documented. Microsoft imposes throttling on Graph API requests—too many requests in a short time and you get a 429 Too Many Requests response with a Retry-After header. For this small export tool it’s not an issue, but for bulk operations I’d need to implement exponential backoff.
## Shared Mailboxes Are Tricky
Shared mailboxes in Microsoft 365 are not regular user mailboxes. They don’t have passwords, can’t be logged into directly, and require special permissions to access via Graph API. You access them by querying `/users/{sharedMailboxEmail}/messages`, but only if:
1. The app has `Mail.Read.Shared` or `Mail.Read.All` permissions
2. The app has been granted access to that specific shared mailbox, or has tenant-wide permissions
3. The shared mailbox exists and is properly configured in the organization
This created a chicken-and-egg problem: to export from a shared mailbox, I need to know its email address. But to know which shared mailboxes exist, I need to query for them. And querying for them requires permissions that might not be granted yet.
The discovery code was an attempt to solve this. If I can programmatically list shared mailboxes, I can populate appsettings.json automatically instead of requiring manual configuration. But Graph API doesn’t make this easy. There’s no `/sharedMailboxes` endpoint. You have to query users and filter by recipient type, or query mailbox folders and infer from delegate access.
I didn’t fully solve this by end of Friday, but I established the problem and prototyped some approaches.
## File Output and JSON Serialization
The export logic writes each email as a JSON object in an array. Used System.Text.Json for serialization—fast, built into .NET, no external dependencies. The output format is an array of objects with properties like `Subject`, `From`, `ReceivedDateTime`, `Body`, `ToRecipients`, etc.
File naming uses the pattern `{mailboxEmail}_{timestamp}.json`. Timestamp is ISO 8601 format to ensure uniqueness and sortability. Saved to the current directory by default, but could be configured.
I didn’t implement incremental exports—every run exports all emails. For large mailboxes this is inefficient. A better approach would be tracking the last export timestamp and only fetching newer emails (`?$filter=receivedDateTime gt {lastExportTime}`). But for a first version, full export is simpler.
## Documentation-Heavy by Design
The project is documentation-heavy. The initial commit contained substantial documentation—PROJECT_SUMMARY.md and README.md representing the majority of the content. Claude Code then added LEARNING_PLAN.md and LEARNING_NOTES.md, plus ADMIN_SETUP_GUIDE.md.
Total: roughly 3,700 lines of documentation, 350 lines of code. A 10:1 ratio. This feels right for a learning project where understanding the domain is more important than shipping features.
The code itself is straightforward—mostly Graph SDK calls and JSON serialization. The complexity is in understanding Microsoft’s authentication and permission model, which is why the documentation is so heavy. And since I’m not writing the code myself, the documentation becomes the primary artifact of learning.
—
**Project**: outlook-exporter-2
**Week**: week #1
**Commits**: 6 (all on Friday, October 17)
**Status**: Initial version working for single and multiple mailboxes. Shared mailbox access not yet working—permissions or discovery logic need refinement.

Deixe um comentário