mirror of
https://github.com/domainaware/parsedmarc.git
synced 2026-09-29 00:50:31 +00:00
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:
+64
-3
@@ -19,13 +19,18 @@ from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from tempfile import NamedTemporaryFile, mkdtemp
|
||||
from typing import BinaryIO, cast
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lxml import etree # type: ignore[import-untyped]
|
||||
|
||||
import parsedmarc
|
||||
from parsedmarc.mail import MaildirConnection
|
||||
from parsedmarc.types import AggregateReport, FailureReport, SMTPTLSReport
|
||||
from parsedmarc.mail import MaildirConnection, MSGraphConnection
|
||||
from parsedmarc.types import (
|
||||
AggregateReport,
|
||||
FailureReport,
|
||||
ParsingResults,
|
||||
SMTPTLSReport,
|
||||
)
|
||||
|
||||
# Detect if running in GitHub Actions to skip DNS lookups
|
||||
OFFLINE_MODE = os.environ.get("GITHUB_ACTIONS", "false").lower() == "true"
|
||||
@@ -2825,6 +2830,62 @@ class TestEmailResultsErrorBranches(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestEmailResultsViaMsGraph(unittest.TestCase):
|
||||
"""email_results_via_msgraph() shares its
|
||||
subject/message/attachment-building logic with email_results() via the
|
||||
extracted _build_report_email_content() helper, so both transports stay
|
||||
in lockstep instead of drifting into two different sets of defaults."""
|
||||
|
||||
@staticmethod
|
||||
def _results() -> ParsingResults:
|
||||
return {
|
||||
"aggregate_reports": [],
|
||||
"failure_reports": [],
|
||||
"smtp_tls_reports": [],
|
||||
}
|
||||
|
||||
def testEmailResultsViaMsGraphBuildsSameContentAsEmailResults(self):
|
||||
connection = MagicMock(spec=MSGraphConnection, mailbox_name="mb@example.com")
|
||||
results = self._results()
|
||||
|
||||
parsedmarc.email_results_via_msgraph(results, connection, ["admin@example.com"])
|
||||
|
||||
connection.send_message.assert_called_once()
|
||||
graph_kwargs = connection.send_message.call_args.kwargs
|
||||
|
||||
with patch("parsedmarc.send_email") as mock_send_email:
|
||||
parsedmarc.email_results(
|
||||
results,
|
||||
host="smtp.example.com",
|
||||
mail_from="from@example.com",
|
||||
mail_to=["admin@example.com"],
|
||||
)
|
||||
mock_send_email.assert_called_once()
|
||||
smtp_kwargs = mock_send_email.call_args.kwargs
|
||||
|
||||
self.assertEqual(graph_kwargs["subject"], smtp_kwargs["subject"])
|
||||
self.assertEqual(graph_kwargs["plain_message"], smtp_kwargs["plain_message"])
|
||||
self.assertEqual(
|
||||
graph_kwargs["attachments"][0][0],
|
||||
smtp_kwargs["attachments"][0][0],
|
||||
)
|
||||
self.assertEqual(graph_kwargs["message_to"], ["admin@example.com"])
|
||||
self.assertEqual(graph_kwargs["message_from"], "mb@example.com")
|
||||
|
||||
def testEmailResultsViaMsGraphAppendsZipExtension(self):
|
||||
connection = MagicMock(spec=MSGraphConnection, mailbox_name="mb@example.com")
|
||||
|
||||
parsedmarc.email_results_via_msgraph(
|
||||
self._results(),
|
||||
connection,
|
||||
["admin@example.com"],
|
||||
attachment_filename="report",
|
||||
)
|
||||
|
||||
graph_kwargs = connection.send_message.call_args.kwargs
|
||||
self.assertEqual(graph_kwargs["attachments"][0][0], "report.zip")
|
||||
|
||||
|
||||
class TestAppendJson(unittest.TestCase):
|
||||
"""append_json writes new files cleanly and merges into existing
|
||||
JSON arrays without breaking valid JSON."""
|
||||
|
||||
Reference in New Issue
Block a user