Skip to content

Commit 6ee2393

Browse files
committed
docs: propose native Python quarantine read sample
1 parent b002c6a commit 6ee2393

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Native Python quarantine access: discussion sample
2+
3+
This small extract demonstrates an Exchange quarantine list request using Azure
4+
Identity and Kiota. It authenticates with the caller's own app and sends HTTP
5+
directly; it does not execute PowerShell or import ExchangeOnlineManagement.
6+
7+
The proposal asks where supported Python quarantine management should belong and
8+
which API contract Microsoft recommends. The Exchange route used here is an
9+
internal module transport, separate from Graph and the public Admin API v2 preview.
10+
This sample does not add an API to `GraphServiceClient` or the generated SDK.
11+
12+
The underlying lab client successfully completed one list request on 2026-10-16.
13+
This extracted sample is validated offline only. It covers one read, prints no
14+
message data, and deliberately omits paging, CLIXML parsing, export and mutations.
15+
16+
## Run in a commercial-cloud lab
17+
18+
Use your own single-tenant public-client app with delegated `Exchange.Manage`
19+
consent and a signed-in lab account with the required Exchange RBAC permissions.
20+
This management permission is broader than the sample's read-only operation.
21+
The script does not create an app or grant permissions.
22+
23+
```powershell
24+
python -m pip install azure-identity httpx microsoft-kiota-abstractions microsoft-kiota-authentication-azure microsoft-kiota-http
25+
python docs/proposals/quarantine/quarantine_read.py --tenant-id '<tenant-guid>' --client-id '<your-app-guid>' --anchor-mailbox 'UPN:admin@contoso.onmicrosoft.com'
26+
```
27+
28+
## Related documented interfaces
29+
30+
- [Quarantine management through PowerShell](https://learn.microsoft.com/en-us/defender-office-365/quarantine-admin-manage-messages-files#use-powershell-to-manage-quarantined-messages)
31+
- [Public Exchange Admin API scope](https://learn.microsoft.com/en-us/exchange/reference/admin-api-overview)
32+
- [Graph analyzed-email list](https://learn.microsoft.com/en-us/graph/api/security-collaborationroot-list-analyzedemails?view=graph-rest-1.0) and [remediation](https://learn.microsoft.com/en-us/graph/api/security-analyzedemail-remediate?view=graph-rest-1.0): related APIs whose coverage, application permissions and Defender licensing must be assessed separately.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Discussion sample: one native Python read through Exchange's internal transport."""
2+
3+
import argparse
4+
import asyncio
5+
import json
6+
from uuid import UUID, uuid4
7+
8+
import httpx
9+
from azure.identity import DeviceCodeCredential
10+
from kiota_abstractions.method import Method
11+
from kiota_abstractions.native_response_handler import NativeResponseHandler
12+
from kiota_abstractions.request_information import RequestInformation
13+
from kiota_authentication_azure.azure_identity_authentication_provider import (
14+
AzureIdentityAuthenticationProvider,
15+
)
16+
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
17+
from kiota_http.kiota_client_factory import KiotaClientFactory
18+
from kiota_http.middleware.options import (
19+
RedirectHandlerOption,
20+
ResponseHandlerOption,
21+
RetryHandlerOption,
22+
)
23+
24+
25+
async def read_quarantine(tenant_id: str, client_id: str, anchor_mailbox: str) -> None:
26+
"""Authenticate with the caller's own app and print a count, without message data."""
27+
tenant_id, client_id = str(UUID(tenant_id)), str(UUID(client_id))
28+
if (
29+
not anchor_mailbox.startswith("UPN:")
30+
or not anchor_mailbox[4:].strip()
31+
or any(character in anchor_mailbox for character in "\r\n")
32+
):
33+
raise ValueError(
34+
"anchor_mailbox must be a nonempty UPN:<lab-user> routing hint"
35+
)
36+
37+
request = RequestInformation(Method.POST)
38+
request.url = (
39+
f"https://outlook.office365.com/adminapi/beta/{tenant_id}/InvokeCommand"
40+
)
41+
for name, value in {
42+
"Accept": "application/json",
43+
"Accept-Charset": "UTF-8",
44+
"Content-Type": "application/json",
45+
"X-CmdletName": "Get-QuarantineMessage",
46+
"X-AnchorMailbox": anchor_mailbox,
47+
"X-ResponseFormat": "auto",
48+
"X-SerializationLevel": "Partial",
49+
"Prefer": "odata.maxpagesize=1000",
50+
"client-request-id": str(uuid4()),
51+
}.items():
52+
request.headers.add(name, value)
53+
request.content = json.dumps(
54+
{
55+
"CmdletInput": {
56+
"CmdletName": "Get-QuarantineMessage",
57+
"Parameters": {"Page": 1, "PageSize": 1},
58+
}
59+
}
60+
).encode("utf-8")
61+
request.add_request_options(
62+
[
63+
ResponseHandlerOption(NativeResponseHandler()),
64+
RedirectHandlerOption(should_redirect=False),
65+
RetryHandlerOption(max_retries=3),
66+
]
67+
)
68+
69+
with DeviceCodeCredential(
70+
tenant_id=tenant_id, client_id=client_id
71+
) as credential:
72+
authentication = AzureIdentityAuthenticationProvider(
73+
credential,
74+
scopes=["https://outlook.office365.com/.default"],
75+
allowed_hosts=["outlook.office365.com"],
76+
)
77+
async with KiotaClientFactory.create_with_default_middleware() as http_client:
78+
adapter = HttpxRequestAdapter(authentication, http_client=http_client)
79+
response = await adapter.send_primitive_async(request, "bytes", None)
80+
if not isinstance(response, httpx.Response):
81+
raise TypeError("Expected the native HTTPX response")
82+
try:
83+
response.raise_for_status()
84+
payload = response.json()
85+
if not isinstance(payload, dict):
86+
raise TypeError("Expected an Exchange response object")
87+
if "error" in payload or payload.get("@adminapi.errors"):
88+
raise RuntimeError("Exchange returned a command error")
89+
records = payload.get("value")
90+
if not isinstance(records, list) or any(
91+
not isinstance(record, dict) for record in records
92+
):
93+
raise RuntimeError("Expected a value array of records")
94+
if any("_cliXml" in record for record in records):
95+
raise RuntimeError(
96+
"PowerShell CLIXML deserialization is outside this sample"
97+
)
98+
if payload.get("@odata.nextLink"):
99+
raise RuntimeError("Transport continuation is outside this sample")
100+
request_id = response.headers.get("request-id") or response.headers.get(
101+
"x-ms-request-id"
102+
)
103+
print(f"Returned {len(records)} record(s).")
104+
print(f"Request ID: {request_id or '(not supplied)'}")
105+
finally:
106+
await response.aclose()
107+
108+
109+
if __name__ == "__main__":
110+
parser = argparse.ArgumentParser(description=__doc__)
111+
parser.add_argument("--tenant-id", required=True)
112+
parser.add_argument("--client-id", required=True)
113+
parser.add_argument("--anchor-mailbox", required=True)
114+
arguments = parser.parse_args()
115+
asyncio.run(
116+
read_quarantine(
117+
arguments.tenant_id, arguments.client_id, arguments.anchor_mailbox
118+
)
119+
)

0 commit comments

Comments
 (0)