Your Security Doc Is Lying (And You Probably Don't Know It)
The Starting Point
I had a client-facing document — the kind you hand to a customer's security team so they can approve your integration with their Azure tenant. It covered how we connect to Microsoft 365 Exchange Online using MS Graph for email operations. App-only auth, shared mailbox, Application Access Policies, the works.
The document looked professional. Versioned, classification headers, clean structure. But I hadn't actually cross-referenced it against what the code does in a while, and the platform had evolved. So I decided to sit down and audit it properly — read the doc, read the code, and see if they still agreed.
They didn't.
What Actually Happened
The permissions were wrong
The document listed the required API permissions as:
Mail.Send (Application permission) Mail.Read or Mail.ReadWrite (Application permission)
That "or" is doing a lot of heavy lifting. In the actual code, after the system fetches inbound emails, it calls markAsRead() on each message via the Graph API. That's a write operation. Mail.Read alone would fail silently or throw a 403.
So the correct answer is Mail.ReadWrite — not optional, not "or". If a customer set up their app with just Mail.Read based on this document, inbound processing would partially work but messages would never get marked as read, leading to duplicate processing on every poll cycle.
The data residency claim was flat wrong
The document contained this line in the architecture section:
"No email infrastructure or mailbox data is replicated outside Microsoft 365."
This is simply not true. The inbound email processor copies email bodies (HTML and plain text), file attachments, and inline images into the application's own database and file storage. That's the whole point — emails become case records that users interact with in the platform.
A customer security team reading that line would reasonably conclude their email content stays in M365. It doesn't. It's stored in the app's database and configurable storage backend (local disk or S3-compatible).
The fix was honest but not alarming:
"Email message content is stored within the platform to support case management workflows. No mailbox-level replication or synchronisation is performed beyond the retrieval of individual messages."
Half the document was about the wrong integration
This was the biggest surprise. The document had two halves: the email integration guide (sections 1-11) and a "Cyber Security & Restricted Access Assurance Document" (sections 1-13). The second half was supposed to provide security assurance for the same integration.
Except it wasn't about email at all. It was about SharePoint.
The whole second section referenced Sites.Selected permissions, document libraries, folder segmentation, and site-level access restrictions. There is zero SharePoint integration in the codebase. No SharePoint SDK, no document library access, nothing.
It had clearly been copied from a SharePoint integration template and never adapted. The section headings were even still SharePoint-specific — one was literally titled "Document Library / Folder Segmentation" while the content underneath talked about reading emails from a mailbox.
The rewrite
I rewrote the entire security assurance section from scratch, keeping the same structure and tone (it's aimed at infosec teams, so it needs to be formal) but making every section actually describe what the email integration does:
- Permissions: Explicitly named
Mail.ReadWriteandMail.Sendinstead of vague "application permissions" - Scope: Described the three operations (fetch inbound, send outbound, mark as read) instead of document library access
- Data residency: Honest about email content being stored in the platform
- Threat table: Rewritten for email-specific threats (unauthorised sending, lateral mailbox access) instead of SharePoint threats (document exposure, site enumeration)
The leftovers
After the client updated the document, I reviewed it again. Two SharePoint ghosts survived:
- Section 4.3 was still titled "Document Library / Folder Segmentation" — the content was correct (email operations) but the heading was pure SharePoint
- Section 6 still said "removing site permissions" instead of "removing the Application Access Policy"
Small things, but exactly the kind of detail a sharp security reviewer would catch and question.
Also noticed the section numbering in part one jumped from 9 to 11 — section 10 was missing entirely.
What Worked
The approach was simple: read the document, read the code, diff them in my head.
The codebase has a clean email integration module with three drivers (MS Graph, a webhook-based provider, and Google Workspace). The MS Graph driver uses the official Microsoft Graph SDK with ClientCredentialContext for auth. Configuration lives in a settings table with env fallbacks, managed through an admin UI.
The key files that told the real story:
- The Graph driver class — showed exactly which API calls are made and what permissions they need
- The inbound email processor — revealed the full data flow: fetch message → match or create case → save body + attachments → mark as read
- The outbound email service — confirmed Mail.Send is used for replies and notifications
- The admin settings controller — showed how credentials are actually stored and configured
Cross-referencing these against the document made every inaccuracy obvious.
What I Learned
-
Security documents drift from reality faster than you'd expect. The code had evolved (new drivers, attachment handling, inline image processing) but the document was frozen at an earlier, simpler version of the integration.
-
Copy-paste from templates is dangerous for security docs. The SharePoint content was clearly from a template library. Someone swapped the title and introduction but left the technical sections untouched. In a security review, this would tank credibility.
-
"Mail.Read or Mail.ReadWrite" is a common trap. If you're doing anything beyond reading — marking as read, moving messages, updating flags — you need ReadWrite. The "or" in Microsoft's docs makes it sound optional. It's not if your app mutates mailbox state.
-
Data residency claims need to be precise. "No data leaves M365" sounds reassuring but if your app stores email content for workflow purposes, that's replication. Better to be upfront about what's stored and why than to make a claim that doesn't survive a code review.
-
Auditing docs against code should be a regular practice, not a one-off. Every time the integration evolves, the customer-facing documentation should be checked. Otherwise you end up with a document that describes a system that doesn't exist.
Tech Notes
- Stack: Laravel 12 with modular architecture, MS Graph SDK for PHP
- Auth: OAuth 2.0 client credentials flow (application-only, no user context)
- Permissions needed:
Mail.ReadWrite+Mail.Send(Application), scoped via Exchange Application Access Policy - Inbound: Scheduled polling command (fetches messages since last run via OData filter)
- Outbound: Service class with pluggable drivers
- Storage: Email content persisted to app database; attachments to configurable local/S3 storage
Zero Focus Dev
Breaking things, learning fast, sharing it all.