Refresh Microsoft Graph docs: national clouds, examples, troubleshooting (#826)

* Send report summary via Microsoft Graph; make Graph failures observable

Two related fixes shipped together:

Send via Graph: the periodic DMARC summary email can now be sent
through the already-authenticated Microsoft Graph mailbox connection
(MSGraphConnection.send_message(), /users/{mailbox}/sendMail) instead
of only SMTP. Triggered when [msgraph] is configured and [smtp] has a
`to` value but no `host` -- SMTP is always preferred when `host` is
set, with no automatic fallback to Graph on SMTP failure. Reuses the
same connection used for reading; no new send-only config mode.
email_results()'s SMTP behavior is unchanged; a new
email_results_via_msgraph() shares its content-building logic via a
new _build_report_email_content() helper. Graph's sendMail always
sends as the authenticated mailbox, so [smtp] from is ignored on this
path -- documented, along with the required Mail.Send permissions and
a caveat that delegated auth flows (UsernamePassword/DeviceCode) don't
currently request that scope, so app-only auth is the supported path
for sending. Tracks #472.

Observable Graph failures: MSGraphConnection construction, mailbox
fetch, message send, and --watch failures now catch
ClientAuthenticationError/APIError/httpx.HTTPError specifically and
log one clear ERROR line naming the mailbox, tenant, auth method, and
the Graph request-id/client-request-id when available, instead of a
bare "MS Graph Error"/"Mailbox Error" with no context. Full traceback
still preserved at --debug. --watch previously had no Graph-specific
error handling at all -- a Graph error there crashed with a raw
uncaught traceback; it now exits the same way as the other three
sites. No new config options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Refresh Microsoft Graph docs: national clouds, examples, troubleshooting

The [msgraph] docs were accurate but missed guidance the community has
been asking for:

- graph_url now lists the actual national/sovereign-cloud endpoint
  values (GCC High, DoD, China/21Vianet), with an explicit warning
  that setting it alone is not sufficient -- the Entra ID auth
  endpoint isn't independently configurable in parsedmarc or
  mailsuite, so it always hits the global login.microsoftonline.com.
- A minimal working [msgraph] example for every auth method
  (UsernamePassword, DeviceCode, ClientSecret, Certificate,
  ClientAssertion) -- previously only Certificate had one, entangled
  with the SMTP-sending example.
- A reading-permission matrix alongside the existing sending one, so
  every auth method x own/shared-mailbox combination is explicit in
  one place for both directions.
- An accurate note on the parsedmarc-named token cache: it's a
  deliberate backward-compatibility choice from the 9.11.0 mailsuite
  extraction (mailsuite's own default cache name differs), not a
  migration users need to act on.
- A troubleshooting table for four error scenarios, verified against
  source rather than assumed: admin consent and folder-resolution
  failures are still live and documented with real fixes; the
  event-loop and ISO-timestamp errors are historical, already fixed
  below this project's dependency/version floor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kili
2026-07-14 13:34:56 -04:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 40509f801b
commit 31c928d6fc
6 changed files with 842 additions and 42 deletions
+167 -4
View File
@@ -249,12 +249,44 @@ The full set of configuration options are:
could be a shared mailbox if the user has access to the mailbox
- `graph_url` - str: Microsoft Graph URL. Allows for use of National Clouds (ex Azure Gov)
(Default: https://graph.microsoft.com)
:::{warning}
Setting `graph_url` alone is **not** sufficient for a national/sovereign
cloud tenant. It only changes the Microsoft Graph API root; the
Microsoft Entra ID (Azure AD) token endpoint used for authentication is
not currently configurable in parsedmarc or `mailsuite`, and always
defaults to the global `https://login.microsoftonline.com`. A true
national-cloud deployment also needs its own Entra ID endpoint, so
`graph_url` by itself only helps if your tenant is registered in the
global cloud but you specifically need to reach one of these Graph API
roots (per [Microsoft's national cloud deployment docs][ms-graph-clouds]):
| National cloud | Microsoft Graph URL | Entra ID endpoint (not configurable here) |
|---|---|---|
| Global (default) | `https://graph.microsoft.com` | `https://login.microsoftonline.com` |
| US Government L4 (GCC High) | `https://graph.microsoft.us` | `https://login.microsoftonline.us` |
| US Government L5 (DoD) | `https://dod-graph.microsoft.us` | `https://login.microsoftonline.us` |
| China, operated by 21Vianet | `https://microsoftgraph.chinacloudapi.cn` | `https://login.chinacloudapi.cn` |
[ms-graph-clouds]: https://learn.microsoft.com/en-us/graph/deployments
:::
- `token_file` - str: Path to save the token file
(Default: `.token`)
- `allow_unencrypted_storage` - bool: Allows the Azure Identity
module to fall back to unencrypted token cache (Default: `False`).
Even if enabled, the cache will always try encrypted storage first.
:::{note}
`token_file` stores the serialized authentication record; the
underlying MSAL persistent token cache is separately named
`parsedmarc`, not `mailsuite`'s own default cache name. This is
deliberate: the Graph mailbox backend used to live directly in
parsedmarc, and moved into the `mailsuite` dependency in parsedmarc
9.11.0. Explicitly keeping the `parsedmarc` cache name means tokens
cached before that move keep working after upgrading — there is
nothing to migrate and no action needed on your part.
:::
:::{note}
You must create an app registration in Azure AD and have an
admin grant the Microsoft Graph `Mail.ReadWrite`
@@ -263,6 +295,11 @@ The full set of configuration options are:
username, you must grant the app `Mail.ReadWrite.Shared`.
:::
| Auth method | Reading (own mailbox) | Reading (shared mailbox) |
|---|---|---|
| `UsernamePassword` / `DeviceCode` (delegated) | `Mail.ReadWrite` | `Mail.ReadWrite.Shared` |
| `ClientSecret` / `Certificate` / `ClientAssertion` (application) | `Mail.ReadWrite` (application), scoped via `New-ApplicationAccessPolicy` | same as own mailbox — app-only access is scoped by policy, not by permission name |
:::{tip}
**Troubleshooting connections.** Run with `--verbose` to log a
redacted connection summary (auth method, tenant, client ID,
@@ -273,6 +310,11 @@ The full set of configuration options are:
an Exchange Online-side one), Microsoft Graph SDK requests, and
`httpx` HTTP request lines. Secret values (passwords, client
secrets, certificate passwords) are never written to logs.
Connection, mailbox fetch, message send, and `--watch` failures
each log a single ERROR line naming the mailbox, tenant, auth
method, and the Graph `request-id`/`client-request-id` when
available — worth quoting verbatim when contacting Microsoft
support.
:::
:::{warning}
@@ -295,6 +337,120 @@ The full set of configuration options are:
applies to the `Certificate` and `ClientAssertion` auth methods.
:::
**Sending the summary email via Microsoft Graph.**
When `[msgraph]` is configured and `[smtp]` has a `to` value but no
`host`, the periodic summary email is sent through the same
already-authenticated Graph mailbox connection used for reading
(`/users/{mailbox}/sendMail`), and a copy is saved to Sent Items.
When `[smtp] host` is set, SMTP is used regardless of whether
`[msgraph]` is also configured — SMTP is always preferred, with no
automatic fallback to Graph on SMTP failure.
Example config combining `[msgraph]` with a `[smtp]` section that
only sets `to`/`subject` (no `host`):
```ini
[msgraph]
auth_method = Certificate
client_id = ...
tenant_id = ...
mailbox = dmarc-reports@example.com
certificate_path = /path/to/cert.pem
[smtp]
to = admin@example.com
subject = DMARC Summary
```
Required Microsoft Graph permissions, in addition to the
reading-related permissions documented above:
| Auth method | Reading | Sending (own mailbox) | Sending (shared mailbox) |
|---|---|---|---|
| `UsernamePassword` / `DeviceCode` (delegated) | `Mail.ReadWrite` (+`.Shared` for shared) | `Mail.Send` | `Mail.Send.Shared` |
| `ClientSecret` / `Certificate` / `ClientAssertion` (application) | `Mail.ReadWrite` (application) | `Mail.Send` (application) | `Mail.Send` (application), scoped via `New-ApplicationAccessPolicy` |
:::{warning}
Graph-based sending is only confirmed to work with the app-only
auth methods (`ClientSecret`, `Certificate`, `ClientAssertion`).
The delegated auth methods (`UsernamePassword`, `DeviceCode`)
currently request only the `Mail.ReadWrite`(`.Shared`) scope when
authenticating, not `Mail.Send` — so a delegated connection's
access token will not carry `Mail.Send` even if an administrator
has granted it, and `/sendMail` calls are expected to fail with an
access-denied error regardless of what's granted in Azure AD. Use
an app-only auth method if you need Graph-based sending.
:::
**Minimal example configs.** Each auth method needs a different
minimum set of keys. These read-only examples omit `[smtp]`; see
above for adding Graph-based sending on top of any of them.
`UsernamePassword` (delegated, own mailbox):
```ini
[msgraph]
auth_method = UsernamePassword
client_id = ...
client_secret = ...
user = dmarc-reports@example.com
password = ...
```
`DeviceCode` (delegated, interactive sign-in on first run — `user`
is the account that signs in; `mailbox` is the shared mailbox it
reads, and only needs to differ from `user` to request
`Mail.ReadWrite.Shared` instead of plain `Mail.ReadWrite`):
```ini
[msgraph]
auth_method = DeviceCode
client_id = ...
tenant_id = ...
user = signing-in-user@example.com
mailbox = dmarc-reports@example.com
```
`ClientSecret` (app-only):
```ini
[msgraph]
auth_method = ClientSecret
client_id = ...
tenant_id = ...
client_secret = ...
mailbox = dmarc-reports@example.com
```
`Certificate` (app-only):
```ini
[msgraph]
auth_method = Certificate
client_id = ...
tenant_id = ...
certificate_path = /path/to/cert.pem
mailbox = dmarc-reports@example.com
```
`ClientAssertion` (app-only, short-lived JWT — see the note above
about its unsuitability for `watch` mode):
```ini
[msgraph]
auth_method = ClientAssertion
client_id = ...
tenant_id = ...
client_assertion = ...
mailbox = dmarc-reports@example.com
```
:::{tip}
**Troubleshooting.**
| Error | Cause | Fix |
|---|---|---|
| *"...needs permission to access resources in your organization that only an admin can grant"* / "Admin consent required" | A delegated auth method (`UsernamePassword`, `DeviceCode`) is authenticating with a scope (`Mail.ReadWrite` or `Mail.ReadWrite.Shared`) the tenant admin hasn't consented to yet. | Have an Entra ID admin grant consent: Azure Portal → **Enterprise Applications** → *your app* → **Permissions** → **Grant admin consent**, or `az ad app permission admin-consent --id <CLIENT_ID>`. This is separate from the `New-ApplicationAccessPolicy` step above, which only applies to app-only auth. |
| `ErrorItemNotFound: ... Default folder Root not found` | `mailsuite` can resolve the well-known folders (`Inbox`, `Archive`, `Drafts`, `Sent Items`, `Deleted Items`, `Junk Email`) even when a mailbox's folder hierarchy hasn't fully provisioned, but a **custom, non-well-known** `reports_folder` name still fails to resolve on such a mailbox. | Point `reports_folder` at (or under) one of the six well-known folder names above, or sign into the (shared) mailbox once via Outlook/OWA to force Exchange to provision it, then retry. |
| `RuntimeError: Event loop is closed` | Historical bug, fixed in `mailsuite` 2.0.2. Not reachable with the `mailsuite>=2.2.2` this project requires. | Confirm your installed `mailsuite` version is current (`pip show mailsuite`); upgrade if it's somehow pinned below 2.0.2. |
| Invalid/rejected timestamp in the `since`/`receivedDateTime` filter | Historical bug (parsedmarc [#706](https://github.com/domainaware/parsedmarc/pull/706)/[#708](https://github.com/domainaware/parsedmarc/pull/708)): older versions appended a spurious `Z` to an already-UTC-offset ISO timestamp. Fixed since parsedmarc 9.5.1/9.5.5. | Upgrade parsedmarc if you're on a version older than 9.5.5. |
:::
- `elasticsearch`
- `hosts` - str: A comma separated list of hostnames and ports
or URLs (e.g. `127.0.0.1:9200` or
@@ -371,14 +527,21 @@ The full set of configuration options are:
- `aggregate_topic` - str: The Kafka topic for aggregate reports
- `failure_topic` - str: The Kafka topic for failure reports
- `smtp`
- `host` - str: The SMTP hostname
- `host` - str: The SMTP hostname. Required unless `[msgraph]` is
configured, in which case omitting it sends the summary via
Microsoft Graph instead — see "Sending the summary email via
Microsoft Graph" above.
- `port` - int: The SMTP port (Default: `25`)
- `ssl` - bool: Require SSL/TLS instead of using STARTTLS
- `skip_certificate_verification` - bool: Skip certificate
verification (not recommended)
- `user` - str: the SMTP username
- `password` - str: the SMTP password
- `from` - str: The From header to use in the email
- `user` - str: the SMTP username. SMTP-only; not used when sending
via Microsoft Graph.
- `password` - str: the SMTP password. SMTP-only; not used when
sending via Microsoft Graph.
- `from` - str: The From header to use in the email. SMTP-only.
When sent via Microsoft Graph, the message's `From` is always the
`[msgraph]` mailbox — `[smtp] from` has no effect.
- `to` - list: A list of email addresses to send to
- `subject` - str: The Subject header to use in the email
(Default: `parsedmarc report`)