contextbase-plugin-microsoft-mail 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contextbase_plugin_microsoft_mail-0.2.6.dist-info/METADATA +14 -0
- contextbase_plugin_microsoft_mail-0.2.6.dist-info/RECORD +18 -0
- contextbase_plugin_microsoft_mail-0.2.6.dist-info/WHEEL +4 -0
- plugin_microsoft_mail/__init__.py +1 -0
- plugin_microsoft_mail/binding_config.py +14 -0
- plugin_microsoft_mail/component.py +189 -0
- plugin_microsoft_mail/defs/__init__.py +0 -0
- plugin_microsoft_mail/defs/defs.yaml +1 -0
- plugin_microsoft_mail/models/__init__.py +1 -0
- plugin_microsoft_mail/models/ctx.py +378 -0
- plugin_microsoft_mail/models/translators.py +193 -0
- plugin_microsoft_mail/plugin.json +7 -0
- plugin_microsoft_mail/sources/__init__.py +1 -0
- plugin_microsoft_mail/sources/attachments.py +407 -0
- plugin_microsoft_mail/sources/sync.py +375 -0
- plugin_microsoft_mail/utils/__init__.py +1 -0
- plugin_microsoft_mail/utils/attachments.py +107 -0
- plugin_microsoft_mail/utils/client.py +245 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from asyncio import AbstractEventLoop
|
|
5
|
+
from collections.abc import Awaitable, Mapping
|
|
6
|
+
from dataclasses import make_dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from azure.core.credentials_async import AsyncTokenCredential
|
|
12
|
+
from azure.identity.aio import ClientSecretCredential
|
|
13
|
+
from kiota_abstractions.base_request_configuration import RequestConfiguration
|
|
14
|
+
from kiota_abstractions.headers_collection import HeadersCollection
|
|
15
|
+
from kiota_abstractions.serialization import Parsable
|
|
16
|
+
from kiota_authentication_azure.azure_identity_authentication_provider import (
|
|
17
|
+
AzureIdentityAuthenticationProvider,
|
|
18
|
+
)
|
|
19
|
+
from kiota_serialization_json.json_serialization_writer import JsonSerializationWriter
|
|
20
|
+
from msgraph import GraphServiceClient
|
|
21
|
+
from msgraph.graph_request_adapter import (
|
|
22
|
+
GraphRequestAdapter,
|
|
23
|
+
options as _GRAPH_DEFAULT_OPTIONS,
|
|
24
|
+
)
|
|
25
|
+
from msgraph_core import APIVersion, GraphClientFactory
|
|
26
|
+
from shared_types.client_credentials_auth import ClientCredentialsAuth
|
|
27
|
+
|
|
28
|
+
_GRAPH_SCOPES = ["https://graph.microsoft.com/.default"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_h11_request_adapter(credential: AsyncTokenCredential) -> GraphRequestAdapter:
|
|
32
|
+
"""Build a `GraphRequestAdapter` whose underlying httpx transport is
|
|
33
|
+
HTTP/1.1 only.
|
|
34
|
+
|
|
35
|
+
Force HTTP/1.1 (kiota's default is `http2=True`). Microsoft Graph's
|
|
36
|
+
frontdoor terminates HTTP/2 connections at ~30s with an advisory
|
|
37
|
+
GOAWAY (`error_code=NO_ERROR`, `last_stream_id=2^31-1`; RFC 7540
|
|
38
|
+
§6.8 graceful shutdown). httpcore turns that into a fatal
|
|
39
|
+
`RemoteProtocolError` for the in-flight stream
|
|
40
|
+
(`httpcore/_async/http2.py:350-355` — `stream_id > last_stream_id`
|
|
41
|
+
is unreachable when `last_stream_id` is `HIGHEST_ALLOWED_STREAM_ID`),
|
|
42
|
+
and kiota_http's `RetryHandler` doesn't catch transport exceptions
|
|
43
|
+
(it retries on status codes 429/503/504 only). HTTP/1.1 keep-alive
|
|
44
|
+
has no equivalent server-driven termination — empirically, a single
|
|
45
|
+
TCP connection comfortably serves 600+ requests over ~40s with the
|
|
46
|
+
same per-request latency as HTTP/2. Full empirical breakdown at
|
|
47
|
+
`internal/in-progress/2026-05-14-codecreators-komal-first-sync.md`
|
|
48
|
+
(B4 entry).
|
|
49
|
+
"""
|
|
50
|
+
raw = httpx.AsyncClient(
|
|
51
|
+
http2=False,
|
|
52
|
+
base_url=f"https://graph.microsoft.com/{APIVersion.v1.value}",
|
|
53
|
+
)
|
|
54
|
+
wrapped = GraphClientFactory.create_with_default_middleware(
|
|
55
|
+
api_version=APIVersion.v1,
|
|
56
|
+
client=raw,
|
|
57
|
+
options=_GRAPH_DEFAULT_OPTIONS,
|
|
58
|
+
)
|
|
59
|
+
auth_provider = AzureIdentityAuthenticationProvider(
|
|
60
|
+
credential, scopes=_GRAPH_SCOPES
|
|
61
|
+
)
|
|
62
|
+
return GraphRequestAdapter(auth_provider, client=wrapped)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _query_parameters_from_mapping(
|
|
66
|
+
query_params: Mapping[str, object | None] | None,
|
|
67
|
+
) -> object | None:
|
|
68
|
+
values = [
|
|
69
|
+
(f"param_{index}", name, value)
|
|
70
|
+
for index, (name, value) in enumerate((query_params or {}).items())
|
|
71
|
+
if value is not None
|
|
72
|
+
]
|
|
73
|
+
if not values:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
encoded_name_by_field = {
|
|
77
|
+
field_name: quote(name, safe="") for field_name, name, _value in values
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def get_query_parameter(_self: object, original_name: str) -> str:
|
|
81
|
+
return encoded_name_by_field.get(original_name, original_name)
|
|
82
|
+
|
|
83
|
+
query_type = make_dataclass(
|
|
84
|
+
"_GraphQueryParameters",
|
|
85
|
+
[(field_name, object) for field_name, _name, _value in values],
|
|
86
|
+
namespace={"get_query_parameter": get_query_parameter},
|
|
87
|
+
frozen=True,
|
|
88
|
+
)
|
|
89
|
+
return query_type(
|
|
90
|
+
**{field_name: value for field_name, _name, value in values},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _request_configuration(
|
|
95
|
+
*,
|
|
96
|
+
query_params: Mapping[str, object | None] | None = None,
|
|
97
|
+
prefer_header: str | None = None,
|
|
98
|
+
) -> RequestConfiguration[Any]:
|
|
99
|
+
cfg: RequestConfiguration[Any] = RequestConfiguration()
|
|
100
|
+
cfg.headers = HeadersCollection()
|
|
101
|
+
cfg.query_parameters = _query_parameters_from_mapping(query_params)
|
|
102
|
+
if prefer_header:
|
|
103
|
+
cfg.headers.add("Prefer", prefer_header)
|
|
104
|
+
return cfg
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SyncGraphMailClient:
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
auth: ClientCredentialsAuth,
|
|
112
|
+
tenant_id: str,
|
|
113
|
+
mailbox_user_id: str,
|
|
114
|
+
):
|
|
115
|
+
self._auth = auth
|
|
116
|
+
self._tenant_id = tenant_id
|
|
117
|
+
self._mailbox_user_id = mailbox_user_id
|
|
118
|
+
self._loop: AbstractEventLoop | None = None
|
|
119
|
+
self._credential: ClientSecretCredential | None = None
|
|
120
|
+
self._client: GraphServiceClient | None = None
|
|
121
|
+
|
|
122
|
+
def __enter__(self) -> SyncGraphMailClient:
|
|
123
|
+
self._loop = asyncio.new_event_loop()
|
|
124
|
+
self._loop.run_until_complete(self._open())
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
128
|
+
assert self._loop is not None
|
|
129
|
+
self._loop.run_until_complete(self._close())
|
|
130
|
+
self._loop.close()
|
|
131
|
+
|
|
132
|
+
async def _open(self) -> None:
|
|
133
|
+
self._credential = ClientSecretCredential(
|
|
134
|
+
tenant_id=self._tenant_id,
|
|
135
|
+
client_id=self._auth.client_id,
|
|
136
|
+
client_secret=self._auth.client_secret,
|
|
137
|
+
)
|
|
138
|
+
self._client = GraphServiceClient(
|
|
139
|
+
request_adapter=_build_h11_request_adapter(self._credential),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def _close(self) -> None:
|
|
143
|
+
if self._credential is not None:
|
|
144
|
+
await self._credential.close()
|
|
145
|
+
|
|
146
|
+
def _run(self, request: Awaitable[Any]) -> Any:
|
|
147
|
+
assert self._loop is not None
|
|
148
|
+
return self._loop.run_until_complete(request)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def mailbox_user_id(self) -> str:
|
|
152
|
+
return self._mailbox_user_id
|
|
153
|
+
|
|
154
|
+
def get_folder_delta_page(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
delta_url: str | None,
|
|
158
|
+
query_params: Mapping[str, object | None],
|
|
159
|
+
prefer_header: str | None,
|
|
160
|
+
) -> Any:
|
|
161
|
+
assert self._client is not None
|
|
162
|
+
builder = self._client.users.by_user_id(self.mailbox_user_id).mail_folders.delta
|
|
163
|
+
if delta_url:
|
|
164
|
+
builder = builder.with_url(delta_url)
|
|
165
|
+
|
|
166
|
+
return self._run(
|
|
167
|
+
builder.get(
|
|
168
|
+
request_configuration=_request_configuration(
|
|
169
|
+
query_params=None if delta_url else query_params,
|
|
170
|
+
prefer_header=prefer_header,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def get_message_delta_page(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
folder_id: str,
|
|
179
|
+
delta_url: str | None,
|
|
180
|
+
query_params: Mapping[str, object | None],
|
|
181
|
+
prefer_header: str | None,
|
|
182
|
+
) -> Any:
|
|
183
|
+
assert self._client is not None
|
|
184
|
+
builder = (
|
|
185
|
+
self._client.users.by_user_id(self.mailbox_user_id)
|
|
186
|
+
.mail_folders.by_mail_folder_id(folder_id)
|
|
187
|
+
.messages.delta
|
|
188
|
+
)
|
|
189
|
+
if delta_url:
|
|
190
|
+
builder = builder.with_url(delta_url)
|
|
191
|
+
|
|
192
|
+
return self._run(
|
|
193
|
+
builder.get(
|
|
194
|
+
request_configuration=_request_configuration(
|
|
195
|
+
query_params=None if delta_url else query_params,
|
|
196
|
+
prefer_header=prefer_header,
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def get_attachment_full(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
message_id: str,
|
|
205
|
+
attachment_id: str,
|
|
206
|
+
prefer_header: str | None,
|
|
207
|
+
) -> Any:
|
|
208
|
+
"""GET /messages/{m}/attachments/{a} — returns the full attachment object
|
|
209
|
+
including contentBytes (for fileAttachment), contentId, contentLocation."""
|
|
210
|
+
assert self._client is not None
|
|
211
|
+
builder = (
|
|
212
|
+
self._client.users.by_user_id(self.mailbox_user_id)
|
|
213
|
+
.messages.by_message_id(message_id)
|
|
214
|
+
.attachments.by_attachment_id(attachment_id)
|
|
215
|
+
)
|
|
216
|
+
return self._run(
|
|
217
|
+
builder.get(
|
|
218
|
+
request_configuration=_request_configuration(
|
|
219
|
+
prefer_header=prefer_header,
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def graph_object_to_payload(obj: object) -> dict[str, Any]:
|
|
226
|
+
if isinstance(obj, Mapping):
|
|
227
|
+
return dict(obj)
|
|
228
|
+
if not isinstance(obj, Parsable):
|
|
229
|
+
raise TypeError(
|
|
230
|
+
"Expected a Microsoft Graph Kiota Parsable object or mapping; "
|
|
231
|
+
f"got {type(obj).__name__}."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
writer = JsonSerializationWriter()
|
|
235
|
+
writer.write_object_value(None, obj)
|
|
236
|
+
payload = writer.value if writer.value is not None else writer.writer
|
|
237
|
+
if not isinstance(payload, dict):
|
|
238
|
+
raise RuntimeError(
|
|
239
|
+
"Microsoft Graph Kiota serializer returned a non-object payload."
|
|
240
|
+
)
|
|
241
|
+
raw = dict(payload)
|
|
242
|
+
additional_data = getattr(obj, "additional_data", None)
|
|
243
|
+
if isinstance(additional_data, dict):
|
|
244
|
+
raw["additional_data"] = dict(additional_data)
|
|
245
|
+
return raw
|