AI

Reading Outlook attachments in Claude, three workarounds that work

Claude Desktop's Microsoft 365 connector reads email body but not attachments. Three workarounds exist. Two are blocked in most secure tenants by design. The path that lasts is registering your own Entra ID app with Mail.Read delegated. Here is the architecture, the failure modes, and the PowerShell that runs.

Quick answers

Why does this matter? Email attachments carry roughly 30-40% of finance, legal, and exec workflows. Claude Desktop reads the body, returns nothing on the attachment. The agent looks broken right when people are starting to trust it.

What should you do? Register a single-tenant Entra ID app, add Mail.Read as a delegated permission, get admin consent once. Hand the client_id to PowerShell or to a small MCP server. Done.

What is the biggest risk? Teams try the default Microsoft Graph PowerShell flow, get denied at the tenant, and assume Claude can't do this work. Wrong conclusion. The denial is correct security posture, not a missing capability.

Where do most people go wrong? Asking IT to consent to the multi-tenant Microsoft Graph Command Line Tools app. Right answer is asking for a single-tenant app the company owns and can revoke.

The dead-end most people hit first

You ask Claude Desktop to read the .docx the lawyer sent over. The model says “I will look at the email and the attachment now.” It calls the Microsoft 365 connector. It comes back with a polished summary of the email body. The attachment is missing.

You ask again. Same answer. You point at the paperclip. The model apologises, says it will fetch the attachment, calls the connector again, and again returns the body summary. No file. No bytes.

That’s not a hallucination. That’s the connector doing exactly what its read_resource interface allows. The MCP server loaded into Claude Desktop accepts these URI prefixes, none of which expose attachments:

mail:///messages/   mail:///folders/   site:   file:   page:
drive:   calendar:   meeting-transcript:   teams:///teams/   teams:///chats/

There’s no attachments: scheme. Suffixing /attachments to a mail:///messages/ URI matches the prefix and silently returns the email metadata unchanged. The suffix is ignored. No error, no warning, just a tidy-looking response that omits the thing you wanted.

I checked the public MCP registry on May 4, 2026. Searches for “outlook”, “microsoft 365”, and “outlook/email/attachment/microsoft/graph” returned zero matches. As of this writing, no community MCP wraps the attachment endpoint.

If you’ve spent any time deploying Claude Desktop into a Microsoft 365 environment, this is the moment a finance lead or general counsel raises an eyebrow and asks whether you tried this for real before promising the agent could do contract review. Fair question. The body-only behaviour is consistent across the Outlook connector, the SharePoint connector, and the Teams connector. Anything binary that’s nested inside a message lives outside the connector’s read window.

The fix isn’t waiting for Anthropic or Microsoft to ship the missing wrapper. It’s routing around it.

Why this is a missing wrapper, not a Microsoft bug

Microsoft Graph itself has the endpoint. It’s documented right here on Microsoft Learn:

GET /me/messages/{id}/attachments/{aid}/$value

That returns the raw bytes. Or, if you use the JSON variant, you get a base64-encoded contentBytes field on the attachment object. Either way, Microsoft has shipped this for years. The endpoint isn’t experimental, it isn’t preview, it’s part of the v1.0 surface.

What’s missing is the MCP layer that wraps that endpoint and presents it as a read_resource URI Claude Desktop can call. Building one is roughly a day of work with the MCP server SDK once you have a tenant-side app registered. I’ll come back to that in the last section.

For now, the practical question is what to do this week. Three paths exist. Two are usually blocked in tenants that take security seriously. The third works if your IT team is willing to spend half an hour.

Three workarounds, ranked by what survives

Workaround 1: Microsoft Graph PowerShell, and why your admin will say no

The first instinct most engineers have is to skip MCP and just script it. Open PowerShell. Install the SDK. Sign in. Pull the attachment. Hand the file to Claude as a regular file:// URI. Done.

The script is short:

Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "Mail.Read"
$msg = Get-MgUserMessage -UserId "me" -Filter "subject eq 'Q3 supplier red-line'" -Top 1
$atts = Get-MgUserMessageAttachment -UserId "me" -MessageId $msg.Id
foreach ($a in $atts) {
    $bytes = [Convert]::FromBase64String($a.AdditionalProperties.contentBytes)
    [System.IO.File]::WriteAllBytes((Join-Path $env:TEMP $a.Name), $bytes)
}

The line that fails is Connect-MgGraph -Scopes "Mail.Read".

What happens behind that single command: the SDK redirects you to Azure AD to sign in, but it signs in as a Microsoft application. The app it uses is multi-tenant, owned by Microsoft, and named “Microsoft Graph Command Line Tools”. Its App ID is 14d82eec-204b-4c2f-b7e8-296a70dab67e.

In a fresh tenant where everything is permitted by default, the user clicks through, grants consent, and the script runs.

In any tenant that’s been hardened for SOC 2, PE-required cyber-insurance, or enterprise AI governance, one of three things happens instead:

  1. The app is blocked outright by Conditional Access. The error reads: “Your administrator has configured the application Microsoft Graph Command Line Tools (14d82eec-…) to block users unless they are specifically granted access to it.” (AADSTS50105.)
  2. Mail.Read requires admin consent. The user sees: “Need admin approval. Microsoft Graph Command Line Tools needs permission to access resources in your organization that only an admin can grant.” (AADSTS90094.)
  3. The app just doesn’t appear in the user’s sign-in surface because it’s been hidden in user consent settings.

When this happens, the natural reaction is frustration with IT. Don’t go there. The denial is correct.

Granting Mail.Read to a multi-tenant Microsoft-owned app means anyone in the tenant who runs Connect-MgGraph from any machine can read their own mailbox. That’s not a security incident on its own (delegated permissions only let you read your own mail anyway). The problem is that you’re handing the keys to an app the company didn’t register, can’t audit, and can’t revoke independently of every other Microsoft Graph integration that uses the same shared CLI app. Most security teams that I’ve worked with would rather have a hundred small, named, auditable apps than one giant Microsoft-owned umbrella.

So yes, the script works in theory. In practice, in any tenant where someone has done the basic SOC 2 evidence collection work, this path is closed by the second time anyone tries it. Stop pushing for consent. Move to workaround 3.

Workaround 2: PowerShell driving Outlook desktop through COM

The second workaround skips Microsoft Graph altogether. It uses Office automation, the same COM interface that VBA macros and old VSTO add-ins relied on, and reads attachments out of the live Outlook profile the user is already signed into.

This works without admin consent, without an app registration, without IT involvement. The script attaches to whatever Outlook session the user has open and walks the message stores looking for a match. When it finds one, it calls attachment.SaveAsFile($path).

There’s exactly one catch, and it’s a big one: COM only exists in Classic Outlook.

Microsoft is partway through replacing Classic Outlook with the new Outlook for Windows, sometimes called Monarch. If you’ve installed a fresh Windows machine in 2025, there’s a good chance the Outlook icon you clicked on is the new one. It looks like the same product. It is not. The new Outlook is a web wrapper around outlook.live.com running inside an Edge WebView2 host. Microsoft was clear about this in the architecture changes documentation: COM, VSTO, and VBA are gone. Only JavaScript web add-ins survive.

The blog Office365ITPros covered this in 2023 when the writing was on the wall. By 2025, PCWorld was running guides on how to prevent the forced installation that Microsoft started shipping with Windows 10 cumulative updates. The transition is happening whether or not your IT team has decided to embrace it.

Which means: before the script runs, detect the flavor.

$classicPath = "$env:ProgramFiles\Microsoft Office\root\Office16\OUTLOOK.EXE"
$newOutlook = Get-AppxPackage -Name "Microsoft.OutlookForWindows" -ErrorAction SilentlyContinue

if (Test-Path $classicPath) {
    Write-Host "CLASSIC: $classicPath"
}
if ($newOutlook) {
    Write-Host "NEW: $($newOutlook.PackageFullName)"
}
if (-not (Test-Path $classicPath)) {
    throw "No Classic Outlook found. COM workaround will fail. Use Mail.Read app instead."
}

If Classic Outlook isn’t there, this entire workaround is dead. Skip to workaround 3 right now.

If Classic is there, the actual extraction script is straightforward. It walks every store, every folder, looks for messages matching a subject or sender filter, and saves attachments to a temp directory. The full version is roughly 90 lines of PowerShell. Here’s the spine:

$ol = New-Object -ComObject Outlook.Application
$ns = $ol.GetNamespace("MAPI")

foreach ($store in $ns.Stores) {
    $root = $store.GetRootFolder()
    $stack = New-Object System.Collections.Stack
    $stack.Push($root)

    while ($stack.Count -gt 0) {
        $folder = $stack.Pop()
        $items = $folder.Items
        $items.Sort("[ReceivedTime]", $true)
        $candidate = $items.Find("[Subject]='Q3 supplier red-line'")

        while ($candidate) {
            if ($candidate.Attachments.Count -gt 0) {
                for ($i = 1; $i -le $candidate.Attachments.Count; $i++) {
                    $a = $candidate.Attachments.Item($i)
                    $a.SaveAsFile((Join-Path $env:TEMP $a.FileName))
                }
            }
            $candidate = $items.FindNext()
        }
        foreach ($sub in $folder.Folders) { $stack.Push($sub) }
    }
}

A few gotchas, learned the hard way:

  1. PowerShell execution policy blocks user-authored scripts. Always invoke with powershell.exe -ExecutionPolicy Bypass -NoProfile -File ....
  2. Exchange senders show up as X.500 addresses, not SMTP. Filtering on -Sender "bmoon" against SenderEmailAddress may fail. Match on SenderName instead, or read SMTP via PropertyAccessor using the property tag http://schemas.microsoft.com/mapi/proptag/0x5D01001F.
  3. Items.Find is fuzzy. Pass [Subject]='Foo' and you get the most-recent-by-default fuzzy match. Want strict equality? You need a separate equality check on .Subject after the find returns.
  4. Sort before you find. $items.Sort("[ReceivedTime]", $true) is required to walk newest-first. Without it, FindNext is non-deterministic.
  5. The same email can appear in multiple stores. Inbox, Archive, search folders. Walk every store and every subfolder, or you’ll miss obvious copies.

Saved attachments may carry company or vendor PII. Delete the temp directory after you’re done:

Remove-Item -Recurse -Force "$env:TEMP\outlook-attachments"

The COM path works. It’s a bit of a kludge, and a stop-gap. Anyone on the new Outlook for Windows hits a dead end the first time the script runs. Plan accordingly.

Workaround 3: register your own Entra ID app with Mail.Read delegated

This is the path that lasts. It works on Classic Outlook, on the new Outlook for Windows, on Outlook on the web, on a Mac with the Outlook 16 desktop client, on a Linux box that’s never had Outlook installed at all. It works because it talks to Microsoft Graph directly, not to any particular flavor of the Outlook client.

Three steps for the IT team, once per tenant:

  1. Register a new app. Entra → Applications → App registrations → New registration. Name it something obvious like <Org> Mail.Read Agent. Single-tenant. No redirect URI needed unless you also want the web flow.
  2. Add the permission. In the new app, API permissions → Add a permission → Microsoft Graph → Delegated permissions → check Mail.Read. Optionally check User.Read for the sign-in name.
  3. Grant admin consent. Same screen, click “Grant admin consent for“. Confirm.

Hand the agent owner the client_id and the tenant_id. That’s it.

The script then pins itself to the new app, not the multi-tenant Microsoft default:

$ClientId = "00000000-0000-0000-0000-000000000000"
$TenantId = "00000000-0000-0000-0000-000000000000"

Connect-MgGraph -ClientId $ClientId -TenantId $TenantId -Scopes "Mail.Read"

$msg = Get-MgUserMessage -UserId "me" `
    -Filter "subject eq 'Q3 supplier red-line'" -Top 1
$atts = Get-MgUserMessageAttachment -UserId "me" -MessageId $msg.Id

foreach ($a in $atts) {
    $bytes = [Convert]::FromBase64String($a.AdditionalProperties.contentBytes)
    $path  = Join-Path $env:TEMP $a.Name
    [System.IO.File]::WriteAllBytes($path, $bytes)
    Write-Host "Saved $($a.Name) ($($bytes.Length) bytes)"
}

Disconnect-MgGraph | Out-Null

For non-interactive environments, swap interactive auth for the device-code flow:

Connect-MgGraph -ClientId $ClientId -TenantId $TenantId `
    -Scopes "Mail.Read" -UseDeviceAuthentication

The user gets a code, opens a URL, signs in once. The token is cached for the session.

A note on what delegated means, because this is the part security teams care about most. A delegated token can only ever read the mailbox of whoever signed in. If Bart signs in, the script can read Bart’s inbox. It cannot reach Pavan’s mailbox even though both users are in the same tenant. Microsoft documents this thoroughly in the permissions reference. The delegated model is exactly what most legal and finance teams want: per-user scope, audit trail tied to a real person, no service-account-impersonates-everyone risk.

There’s also a lineage worth noting. Most companies that have been doing Microsoft 365 work for any length of time already have one or two single-tenant apps registered for things like message-trace reporting, mailbox compliance scans, or distribution-list automation. The shape is identical. Strip the old scope, add Mail.Read, re-consent, and you’ve got the back-end the legal agent needs without registering anything new. For what it’s worth, I’ve seen this pattern reuse cut the IT-coordination time from a week to under an hour. Worth checking before you ask for a brand-new app.

If you want to go further and build an MCP server that wraps this same token flow, the Artic6 blog has a walkthrough on bulk attachment downloading via Graph that covers the message-graph traversal patterns. The MCP wrapper itself is a thin layer on top.

What to do tomorrow morning

Pick the workaround that fits the tenant in front of you.

SituationPath
Classic Outlook installed, IT slow to moveRun the COM script. Stop-gap until Mail.Read app lands.
New Outlook only, no app yetAsk IT for a single-tenant Mail.Read app. Wait. The COM path won’t help.
Either Outlook flavor, Mail.Read app availableUse the Mail.Read app. Both flavors covered with the same script.

If you’re the consultant, your real job is converting “I tried it and it didn’t work” into “here’s the 30-minute IT ticket that fixes it for everyone”. The COM script buys time. The Entra app finishes the job.

Quick clarification before we leave the workarounds. I called this “three workarounds” at the top. Strictly there’s a fourth, the JavaScript Office add-in, which gets its own treatment in the architecture section below. It’s heavier than the three PowerShell paths, which is why it didn’t make the lead. But it does survive the new Outlook transition without changes. So: three PowerShell paths plus one add-in path, properly counted.

A note on the new Outlook for Windows

The transition Microsoft is running deserves a closer look, because it changes the assumption stack for any automation that touches Outlook.

Classic Outlook (sometimes still called “Outlook 2016”, “Outlook 2019”, “Outlook for Microsoft 365 desktop”) is a Win32 application with a COM object model that’s been stable since the early 2000s. VBA macros, VSTO add-ins, COM add-ins, and PowerShell automation all hung off the same Outlook.Application object. That world is closing.

The new Outlook for Windows runs inside Edge WebView2 and is, at its core, Outlook on the web wearing a desktop wrapper. Microsoft has been clear that COM, VSTO, and VBA do not come back. The new Outlook supports only JavaScript web add-ins, which are sandboxed, server-rendered, and require a manifest published through the Office Store or a tenant-internal catalog. A blog post titled “Killing VBA in Outlook” goes through what this means for the macro corpus a lot of finance teams quietly depend on. Spoiler: nothing pleasant.

For most organizations, the practical implication is that any “automation against Outlook” that was built before 2024 will need to be either rewritten against Microsoft Graph or ported to a JavaScript add-in. There’s no third option. If your finance team has a battery of Excel-VBA-driven Outlook scripts that pull receipts, parse statements, or auto-file invoices, they will all stop working at some point in the next two years. Mind you, “stop working” might mean silent failure rather than a sharp error, which is worse.

The good news is that Microsoft Graph plus a single-tenant Entra app covers nearly everything VBA used to. The interface is more verbose. The token model is stricter. But the capability surface is wider, the audit trail is cleaner, and the same code runs across Windows, Mac, and Linux.

If you’re in a position to influence the migration plan at your company, the conversation worth having is not “should we migrate” (Microsoft has decided that for you), but “what gets rebuilt against Graph, what becomes a JavaScript add-in, and what just gets retired”. The answer is usually a mix of all three, weighted heavily toward Graph for anything that touches data integration with Claude or any other AI agent.

What survives, the JavaScript web add-in path

The one extensibility model that comes through the new Outlook transition intact is the JavaScript web add-in. Office add-ins built against the JavaScript API run inside an iframe rendered by the new Outlook host. They have access to a defined surface of mailbox operations: read the body of the open message, list attachments, write into a compose pane, drop something into the user’s calendar. Microsoft maintains a migration guide for moving existing COM functionality into this model.

The relevant phrase there is “list attachments”. The add-in JavaScript API does expose getAttachmentsAsync and getAttachmentContentAsync flows that return attachment bytes (base64) directly to the add-in frontend. So in theory, a small JavaScript Office add-in could be the bridge between the new Outlook UI and a Claude Desktop session.

In practice, three things make that path heavier than it sounds:

  1. The add-in needs to be registered in a tenant catalog or the public Office Store. Tenant-internal add-ins need a manifest XML, a hosting endpoint for the JavaScript bundle, and an admin willing to publish it through the Microsoft 365 admin center. That means cobbling together a manifest, a host, and an admin sign-off. Real work.
  2. The user has to install it. Click around in the ribbon, find “Get Add-ins”, search the catalog, install. Not bad for power users, painful for an exec workflow that’s supposed to “just work”.
  3. The handoff to Claude still needs HTTP. The add-in receives the attachment bytes, but Claude Desktop is a separate process. You either upload the bytes somewhere Claude can read them (OneDrive, an MCP server, a local file via the add-in’s writable scratch space), or you have the user copy-paste into the Claude window manually. Neither is graceful.

For most teams, the JavaScript add-in path is heavier than the Mail.Read Entra app and lighter than nothing. Worth knowing it exists. Not worth building before the simpler Graph path has been tried and rejected. Mind you, if you’re already shipping a tenant-internal Office add-in for other reasons, slipping in the attachment hook is cheap.

What I’d build if I had a week

The catch with all three workarounds: they all pre-suppose that someone, somewhere, is willing to run PowerShell. That’s a reasonable assumption for an internal IT team. It’s not a reasonable assumption for an executive who just wants Claude to read the contract attached to an email.

The cleaner shape is a long-lived MCP server that wraps GET /me/messages/{id}/attachments/{aid}/$value behind the company’s own Entra app, exposes it as a mail-attachment:// URI scheme, and runs as a Windows service or a small container.

Roughly a day of work with the MCP server SDK, including the OAuth dance, the token caching, and a sane retry policy for the hourly Graph rate limit. The hard part isn’t the code. It’s deciding where the server runs (per-user laptop, per-team VM, central tenant-wide service), how it caches tokens, and how it logs access for audit. Same questions you’d ask building any enterprise Claude integration.

I’d add three things on top:

  • A --dry-run mode that lists matched attachments without writing them, so a security team can review what the agent is about to read before any bytes leave Microsoft’s servers.
  • A built-in sensitivity-folder check that refuses to operate against Treasury/, Tax/, Onboarding/, or any folder explicitly tagged as restricted.
  • An MCP-side audit log that records every read_resource call with the calling user, the message ID, the attachment name, and a cryptographic hash of the bytes returned.

That’s the thing I’d hand to a CISO and ask them to sign off on. The first two protect against accidental over-reach. The third gives them the breadcrumb trail they need when a compliance officer asks who looked at what, when. None of that is exotic. It just isn’t built yet, in any MCP I’ve found.

If you’re building this, I’d love to see it. The community needs one good open-source example.

The Microsoft 365 connector lies by omission. It returns email body and silently drops the attachment. That’s a limit, not a bug, and the limit will likely close at some point as either Anthropic or the community ships a proper Graph attachment MCP.

Until then, three paths. Two are blocked in any tenant that takes security seriously, and rightly so. The third works if your IT team is willing to spend half an hour on a single-tenant Entra app with Mail.Read delegated. That’s the path that survives the new Outlook transition, the path that scales beyond one user, and the path that gives a security team something they can audit and revoke.

Pick the right one for the tenant in front of you. Don’t ask IT to consent to the multi-tenant Microsoft Graph CLI app. They’ll say no, and the no is correct.

If you’re working through this in a larger Claude Desktop deployment or trying to organize SharePoint and OneDrive for Claude Cowork, the same architectural reflex applies: register your own apps, never lean on multi-tenant Microsoft defaults, and design for the audit trail before you design for the convenience.

Want help working this out for your organization? Blue Sheen does this kind of plumbing for a living.

About the Author

Amit Kothari is an experienced consultant, advisor, coach, and educator specializing in AI and operations for executives and their companies. With 25+ years of experience and as the founder of Tallyfy (raised $3.6m), he helps mid-size companies identify, plan, and implement practical AI solutions that actually work. Originally British and now based in St. Louis, MO, Amit combines deep technical expertise with real-world business understanding. Read Amit's full bio →

Disclaimer: The content in this article represents personal opinions based on extensive research and practical experience. While every effort has been made to ensure accuracy through data analysis and source verification, this should not be considered professional advice. Always consult with qualified professionals for decisions specific to your situation.

AI advisory services via Blue Sheen.
Contact me Follow