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.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.3
2
+ Name: contextbase-plugin-microsoft-mail
3
+ Version: 0.2.6
4
+ Summary: Microsoft Mail plugin for ContextBase
5
+ Author: Alizain Feerasta
6
+ Author-email: Alizain Feerasta <alizain.feerasta@gmail.com>
7
+ Requires-Dist: azure-identity>=1.25.1
8
+ Requires-Dist: dagster==1.12.14
9
+ Requires-Dist: dagster-dlt==0.28.14
10
+ Requires-Dist: dlt[duckdb]>=1.26.0
11
+ Requires-Dist: msgraph-sdk>=1.56.0
12
+ Requires-Dist: pydantic>=2.12.0
13
+ Requires-Dist: contextbase-shared-plugins==0.2.6
14
+ Requires-Python: >=3.14, <3.15
@@ -0,0 +1,18 @@
1
+ plugin_microsoft_mail/__init__.py,sha256=ElTC8h64p4DKoEbGEjimkj2CIjmT8l_aw0me_JGZfMM,37
2
+ plugin_microsoft_mail/binding_config.py,sha256=NWHO20z3EPLSZwfJS5ckQqq91A05epw8xA-TN9L3zqk,401
3
+ plugin_microsoft_mail/component.py,sha256=_SV5rAICmhtw_M1KeVJVtnOObqo0P3toBv9_Pyev8FM,6754
4
+ plugin_microsoft_mail/defs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ plugin_microsoft_mail/defs/defs.yaml,sha256=QazgvJAFnOXMfZQnZixhJ9vEOKlLJLW2E5lbJ0o4Etw,61
6
+ plugin_microsoft_mail/models/__init__.py,sha256=uzhFJ0GZsyzEbh9e2aDXued2szRLOdK0y4Edu9Au-tg,45
7
+ plugin_microsoft_mail/models/ctx.py,sha256=mjZWlQ9JTUP9kcPryEK3qWCDqUcA5ILAa9ocUWv884I,16193
8
+ plugin_microsoft_mail/models/translators.py,sha256=HKVuE9MZSYWvL4OEJ_OzFLWAiedWYyNrixVoBY8t3RY,7577
9
+ plugin_microsoft_mail/plugin.json,sha256=qmr0yO23xgAmk3fFEEamGsjvtaIpByrCf5Ei-KMA43c,101
10
+ plugin_microsoft_mail/sources/__init__.py,sha256=amRjajxNvz162fVQ7qBKav8u_5B3RlynZovTc_cOdI8,38
11
+ plugin_microsoft_mail/sources/attachments.py,sha256=x5IDgTXxUO1ZRh-dl8F9Ir4d5zVSgZIqOGQH-JceHto,14152
12
+ plugin_microsoft_mail/sources/sync.py,sha256=Mp7wKcz-TWy8WBBC11Mc-BvWhVi_xzvVMcr9vnFpJro,12485
13
+ plugin_microsoft_mail/utils/__init__.py,sha256=6rzVtzFRsQ3u0YcpE-Ehb2Amr0I6nasQ01xRTy3rc2U,57
14
+ plugin_microsoft_mail/utils/attachments.py,sha256=VGMs105v2vaHro0dIstTQ5VXe-EY28khAmWtaKNM4EE,3673
15
+ plugin_microsoft_mail/utils/client.py,sha256=JjR6DSQYeFm-f74i4uhEN4m7lyRjpC2Pu1msX_4N678,8552
16
+ contextbase_plugin_microsoft_mail-0.2.6.dist-info/WHEEL,sha256=i9aSRDivn5iP9LaR1BLQX2GNAuriQWPsFwbbWygTX2k,81
17
+ contextbase_plugin_microsoft_mail-0.2.6.dist-info/METADATA,sha256=dd0tfO1J64U2EVACxLUkiZQ50KOj1MAcqsVl5-6w5JQ,497
18
+ contextbase_plugin_microsoft_mail-0.2.6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.15
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1 @@
1
+ """Microsoft Mail plugin package."""
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field
6
+ from shared_plugins.bindings import BaseBindingConfigModel, NonEmptyText
7
+
8
+ PositiveInt = Annotated[int, Field(strict=True, ge=1)]
9
+
10
+
11
+ class MicrosoftMailBindingConfig(BaseBindingConfigModel):
12
+ tenant_id: NonEmptyText
13
+ mailbox_user_id: NonEmptyText
14
+ initial_message_delta_top: PositiveInt | None = 1000
@@ -0,0 +1,189 @@
1
+ import dagster as dg
2
+ from dagster import AssetExecutionContext
3
+ from dagster_dlt import DagsterDltResource
4
+ from shared_plugins.automation import non_overlapping_automation_condition
5
+ from shared_plugins.bindings import parse_binding_config, require_client_credentials
6
+ from shared_plugins.control_plane import ControlPlaneClient
7
+ from shared_plugins.dlt import resolve_partition_binding, run_dlt_pipeline
8
+ from shared_plugins.naming import (
9
+ dagster_asset_group_name,
10
+ dagster_asset_tags,
11
+ dagster_dlt_asset_key,
12
+ dagster_partition_def_name,
13
+ dagster_pool_name,
14
+ dlt_source_name,
15
+ plugin_id_from_module,
16
+ )
17
+ from shared_plugins.resources import DLT_RESOURCE
18
+
19
+ from .binding_config import MicrosoftMailBindingConfig
20
+ from .sources.attachments import microsoft_mail_attachment_source
21
+ from .sources.sync import microsoft_mail_source
22
+ from .utils.client import SyncGraphMailClient
23
+
24
+ PLUGIN_ID = plugin_id_from_module(__file__)
25
+ SYNC_JOB = "sync"
26
+ ATTACHMENT_CONTENT_JOB = "attachment_content"
27
+
28
+ SYNC_SOURCE_NAME = dlt_source_name(PLUGIN_ID, SYNC_JOB)
29
+ ATTACHMENT_CONTENT_SOURCE_NAME = dlt_source_name(
30
+ PLUGIN_ID,
31
+ ATTACHMENT_CONTENT_JOB,
32
+ )
33
+
34
+ MAIL_FOLDERS_ASSET_KEY = dagster_dlt_asset_key(SYNC_SOURCE_NAME, "mail_folders")
35
+ MESSAGES_ASSET_KEY = dagster_dlt_asset_key(SYNC_SOURCE_NAME, "messages")
36
+ ATTACHMENT_CONTENT_ASSET_KEY = dagster_dlt_asset_key(
37
+ ATTACHMENT_CONTENT_SOURCE_NAME,
38
+ "attachment_content",
39
+ )
40
+
41
+
42
+ def _build_sync_specs(
43
+ partitions_def: dg.PartitionsDefinition,
44
+ automation_condition: dg.AutomationCondition,
45
+ ) -> list[dg.AssetSpec]:
46
+ shared = dict(
47
+ group_name=dagster_asset_group_name(PLUGIN_ID),
48
+ tags=dagster_asset_tags(PLUGIN_ID),
49
+ automation_condition=automation_condition,
50
+ partitions_def=partitions_def,
51
+ )
52
+ return [
53
+ dg.AssetSpec(key=MAIL_FOLDERS_ASSET_KEY, **shared),
54
+ dg.AssetSpec(key=MESSAGES_ASSET_KEY, deps=[MAIL_FOLDERS_ASSET_KEY], **shared),
55
+ ]
56
+
57
+
58
+ def _build_attachment_content_specs(
59
+ partitions_def: dg.PartitionsDefinition,
60
+ automation_condition: dg.AutomationCondition,
61
+ ) -> list[dg.AssetSpec]:
62
+ return [
63
+ dg.AssetSpec(
64
+ key=ATTACHMENT_CONTENT_ASSET_KEY,
65
+ group_name=dagster_asset_group_name(PLUGIN_ID),
66
+ tags=dagster_asset_tags(PLUGIN_ID),
67
+ automation_condition=automation_condition,
68
+ partitions_def=partitions_def,
69
+ deps=[MESSAGES_ASSET_KEY],
70
+ )
71
+ ]
72
+
73
+
74
+ class MicrosoftMailComponent(dg.Component):
75
+ def build_defs(self, context: dg.ComponentLoadContext) -> dg.Definitions:
76
+ partitions_def = dg.DynamicPartitionsDefinition(
77
+ name=dagster_partition_def_name(PLUGIN_ID)
78
+ )
79
+
80
+ sync_specs = _build_sync_specs(
81
+ partitions_def,
82
+ non_overlapping_automation_condition(
83
+ dg.AutomationCondition.on_missing()
84
+ | dg.AutomationCondition.on_cron("*/15 * * * *")
85
+ ),
86
+ )
87
+ attachment_content_specs = _build_attachment_content_specs(
88
+ partitions_def,
89
+ non_overlapping_automation_condition(
90
+ dg.AutomationCondition.cron_tick_passed("*/10 * * * *")
91
+ ),
92
+ )
93
+
94
+ @dg.multi_asset(
95
+ specs=sync_specs,
96
+ can_subset=False,
97
+ name="microsoft_mail_sync",
98
+ pool=dagster_pool_name(PLUGIN_ID),
99
+ )
100
+ def microsoft_mail_sync_assets(
101
+ context: AssetExecutionContext,
102
+ dlt_resource: DagsterDltResource,
103
+ control_plane: dg.ResourceParam[ControlPlaneClient],
104
+ ):
105
+ binding = resolve_partition_binding(
106
+ context=context,
107
+ control_plane=control_plane,
108
+ plugin_id=PLUGIN_ID,
109
+ )
110
+ binding_id = str(binding.binding_id)
111
+ cfg = parse_binding_config(binding, MicrosoftMailBindingConfig)
112
+ auth = require_client_credentials(binding)
113
+
114
+ with SyncGraphMailClient(
115
+ auth=auth,
116
+ tenant_id=cfg.tenant_id,
117
+ mailbox_user_id=cfg.mailbox_user_id,
118
+ ) as client:
119
+ source = microsoft_mail_source(
120
+ binding_id,
121
+ client=client,
122
+ initial_message_delta_top=cfg.initial_message_delta_top,
123
+ )
124
+ yield from run_dlt_pipeline(
125
+ context=context,
126
+ dlt_resource=dlt_resource,
127
+ source=source,
128
+ plugin_id=PLUGIN_ID,
129
+ binding_id=binding_id,
130
+ job_name=SYNC_JOB,
131
+ )
132
+
133
+ @dg.multi_asset(
134
+ specs=attachment_content_specs,
135
+ name="microsoft_mail_attachment_content",
136
+ pool=dagster_pool_name(PLUGIN_ID),
137
+ )
138
+ def microsoft_mail_attachment_content_assets(
139
+ context: AssetExecutionContext,
140
+ dlt_resource: DagsterDltResource,
141
+ control_plane: dg.ResourceParam[ControlPlaneClient],
142
+ ):
143
+ binding = resolve_partition_binding(
144
+ context=context,
145
+ control_plane=control_plane,
146
+ plugin_id=PLUGIN_ID,
147
+ )
148
+ binding_id = str(binding.binding_id)
149
+ cfg = parse_binding_config(binding, MicrosoftMailBindingConfig)
150
+ auth = require_client_credentials(binding)
151
+
152
+ with SyncGraphMailClient(
153
+ auth=auth,
154
+ tenant_id=cfg.tenant_id,
155
+ mailbox_user_id=cfg.mailbox_user_id,
156
+ ) as client:
157
+ source = microsoft_mail_attachment_source(
158
+ binding_id,
159
+ client=client,
160
+ )
161
+ yield from run_dlt_pipeline(
162
+ context=context,
163
+ dlt_resource=dlt_resource,
164
+ source=source,
165
+ plugin_id=PLUGIN_ID,
166
+ binding_id=binding_id,
167
+ job_name=ATTACHMENT_CONTENT_JOB,
168
+ )
169
+
170
+ automation_sensor = dg.AutomationConditionSensorDefinition(
171
+ name="microsoft_mail_automation_sensor",
172
+ target=dg.AssetSelection.assets(
173
+ microsoft_mail_sync_assets,
174
+ microsoft_mail_attachment_content_assets,
175
+ ),
176
+ default_status=dg.DefaultSensorStatus.RUNNING,
177
+ minimum_interval_seconds=30,
178
+ )
179
+
180
+ return dg.Definitions(
181
+ assets=[
182
+ microsoft_mail_sync_assets,
183
+ microsoft_mail_attachment_content_assets,
184
+ ],
185
+ sensors=[automation_sensor],
186
+ resources={
187
+ "dlt_resource": DLT_RESOURCE,
188
+ },
189
+ )
File without changes
@@ -0,0 +1 @@
1
+ type: plugin_microsoft_mail.component.MicrosoftMailComponent
@@ -0,0 +1 @@
1
+ """Microsoft Mail ContextBase row models."""
@@ -0,0 +1,378 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import AwareDatetime, Field, model_validator
6
+ from shared_plugins.models import CtxModel, IdStr, NonNegativeInt
7
+
8
+
9
+ class MailFolderRow(CtxModel):
10
+ ctx_deleted: bool = Field(default=False, alias="_ctx_deleted")
11
+ id: IdStr
12
+ odata_type: str | None = None
13
+ additional_data: dict[str, Any] = Field(default_factory=dict)
14
+ child_folder_count: NonNegativeInt | None = None
15
+ child_folders: list[dict[str, Any]] = Field(default_factory=list)
16
+ display_name: str | None = None
17
+ is_hidden: bool | None = None
18
+ message_rules: list[dict[str, Any]] = Field(default_factory=list)
19
+ messages: list[dict[str, Any]] = Field(default_factory=list)
20
+ multi_value_extended_properties: list[dict[str, Any]] = Field(default_factory=list)
21
+ parent_folder_id: str | None = None
22
+ single_value_extended_properties: list[dict[str, Any]] = Field(default_factory=list)
23
+ total_item_count: NonNegativeInt | None = None
24
+ unread_item_count: NonNegativeInt | None = None
25
+
26
+
27
+ class MessageRow(CtxModel):
28
+ ctx_deleted: bool = Field(default=False, alias="_ctx_deleted")
29
+ id: IdStr
30
+ odata_type: str | None = None
31
+ etag: str | None = None
32
+ additional_data: dict[str, Any] = Field(default_factory=dict)
33
+ attachments: list[dict[str, Any]] = Field(default_factory=list)
34
+ bcc_recipients: list[dict[str, Any]] = Field(default_factory=list)
35
+ body: dict[str, Any] | None = None
36
+ body_preview: str | None = None
37
+ categories: list[str] = Field(default_factory=list)
38
+ cc_recipients: list[dict[str, Any]] = Field(default_factory=list)
39
+ change_key: str | None = None
40
+ conversation_id: str | None = None
41
+ conversation_index: str | None = None
42
+ created_date_time: AwareDatetime | None = None
43
+ extensions: list[dict[str, Any]] = Field(default_factory=list)
44
+ flag: dict[str, Any] | None = None
45
+ from_: dict[str, Any] | None = None
46
+ has_attachments: bool | None = None
47
+ importance: str | None = None
48
+ inference_classification: str | None = None
49
+ internet_message_headers: list[dict[str, Any]] = Field(default_factory=list)
50
+ internet_message_id: str | None = None
51
+ is_delivery_receipt_requested: bool | None = None
52
+ is_draft: bool | None = None
53
+ is_read: bool | None = None
54
+ is_read_receipt_requested: bool | None = None
55
+ last_modified_date_time: AwareDatetime | None = None
56
+ multi_value_extended_properties: list[dict[str, Any]] = Field(default_factory=list)
57
+ parent_folder_id: IdStr
58
+ received_date_time: AwareDatetime | None = None
59
+ reply_to: list[dict[str, Any]] = Field(default_factory=list)
60
+ sender: dict[str, Any] | None = None
61
+ sent_date_time: AwareDatetime | None = None
62
+ single_value_extended_properties: list[dict[str, Any]] = Field(default_factory=list)
63
+ subject: str | None = None
64
+ to_recipients: list[dict[str, Any]] = Field(default_factory=list)
65
+ unique_body: dict[str, Any] | None = None
66
+ web_link: str | None = None
67
+
68
+
69
+ class AttachmentContentRow(CtxModel):
70
+ """Row in the `attachment_content` table.
71
+
72
+ Carries either materialized-attachment data (live row) or a deletion
73
+ signal (tombstone) emitted by the orphan-cleanup arm of the content
74
+ job's candidate query.
75
+
76
+ - Live row: `ctx_deleted=False`, `attachment_id` and `file_path` populated.
77
+ - Tombstone: `ctx_deleted=True`, only the merge-key columns
78
+ (`_ctx_binding_id`, `message_id`) populated; dlt deletes every
79
+ `attachment_content` row matching that merge_key regardless of
80
+ `attachment_id`.
81
+
82
+ `attachment_id` and `file_path` are nullable on the type because
83
+ tombstones don't carry them. The validator enforces that live rows
84
+ (`ctx_deleted=False`) populate both.
85
+ """
86
+
87
+ message_id: IdStr
88
+ attachment_id: IdStr | None = None
89
+ odata_type: str | None = None
90
+ media_content_type: str | None = None
91
+ name: str | None = None
92
+ content_type: str | None = None
93
+ size: NonNegativeInt | None = None
94
+ is_inline: bool | None = None
95
+ content_id: str | None = None
96
+ content_location: str | None = None
97
+ last_modified_date_time: AwareDatetime | None = None
98
+ file_path: str | None = None
99
+ ctx_deleted: bool = Field(default=False, alias="_ctx_deleted")
100
+
101
+ @model_validator(mode="after")
102
+ def _enforce_live_row_invariant(self) -> "AttachmentContentRow":
103
+ if not self.ctx_deleted:
104
+ if self.attachment_id is None or self.file_path is None:
105
+ raise ValueError(
106
+ "Live AttachmentContentRow (ctx_deleted=False) must have "
107
+ "attachment_id and file_path; got "
108
+ f"attachment_id={self.attachment_id!r}, file_path={self.file_path!r}."
109
+ )
110
+ return self
111
+
112
+
113
+ MAIL_FOLDER_COLUMN_DESCRIPTIONS = {
114
+ "child_folder_count": {
115
+ "description": "The number of immediate child mailFolders in the current mailFolder."
116
+ },
117
+ "child_folders": {
118
+ "description": "The collection of child folders in the mailFolder."
119
+ },
120
+ "display_name": {"description": "The mailFolder's display name."},
121
+ "id": {"description": "The mailFolder's unique identifier."},
122
+ "is_hidden": {
123
+ "description": (
124
+ "Indicates whether the mailFolder is hidden. This property can be set "
125
+ "only when creating the folder. Find more information in Hidden mail "
126
+ "folders."
127
+ )
128
+ },
129
+ "message_rules": {
130
+ "description": "The collection of rules that apply to the user's Inbox folder."
131
+ },
132
+ "messages": {"description": "The collection of messages in the mailFolder."},
133
+ "multi_value_extended_properties": {
134
+ "description": (
135
+ "The collection of multi-value extended properties defined for the "
136
+ "mailFolder. Read-only. Nullable."
137
+ )
138
+ },
139
+ "parent_folder_id": {
140
+ "description": "The unique identifier for the mailFolder's parent mailFolder."
141
+ },
142
+ "single_value_extended_properties": {
143
+ "description": (
144
+ "The collection of single-value extended properties defined for the "
145
+ "mailFolder. Read-only. Nullable."
146
+ )
147
+ },
148
+ "total_item_count": {"description": "The number of items in the mailFolder."},
149
+ "unread_item_count": {
150
+ "description": "The number of items in the mailFolder marked as unread."
151
+ },
152
+ }
153
+
154
+ MESSAGE_COLUMN_DESCRIPTIONS = {
155
+ "attachments": {
156
+ "description": (
157
+ "Microsoft Graph attachments expanded inline with the message delta. "
158
+ "Each element is a Graph attachment record (file/reference/item). "
159
+ "Includes inline attachments (e.g. signature images, embedded "
160
+ "screenshots), distinguished by `isInline`. To exclude inline "
161
+ "attachments in a query: `WHERE NOT (a->>'isInline')::bool` over "
162
+ "`jsonb_array_elements(attachments) AS a`."
163
+ )
164
+ },
165
+ "bcc_recipients": {"description": "The Bcc: recipients for the message."},
166
+ "body": {
167
+ "description": (
168
+ "The body of the message. It can be in HTML or text format. Find out "
169
+ "about safe HTML in a message body."
170
+ )
171
+ },
172
+ "body_preview": {
173
+ "description": (
174
+ "The first 255 characters of the message body. It is in text format."
175
+ )
176
+ },
177
+ "cc_recipients": {"description": "The Cc: recipients for the message."},
178
+ "change_key": {"description": "The version of the message."},
179
+ "conversation_id": {
180
+ "description": "The ID of the conversation the email belongs to."
181
+ },
182
+ "conversation_index": {
183
+ "description": "Indicates the position of the message within the conversation."
184
+ },
185
+ "created_date_time": {
186
+ "description": (
187
+ "The date and time the message was created. The date and time "
188
+ "information uses ISO 8601 format and is always in UTC time. For "
189
+ "example, midnight UTC on Jan 1, 2014 is `2014-01-01T00:00:00Z`."
190
+ )
191
+ },
192
+ "extensions": {
193
+ "description": (
194
+ "The collection of open extensions defined for the message. Nullable."
195
+ )
196
+ },
197
+ "flag": {
198
+ "description": (
199
+ "Indicates the status, start date, due date, or completion date for "
200
+ "the message."
201
+ )
202
+ },
203
+ "from_": {
204
+ "description": (
205
+ "The owner of the mailbox from which the message is sent. In most "
206
+ "cases, this value is the same as the sender property, except for "
207
+ "sharing or delegation scenarios. The value must correspond to the "
208
+ "actual mailbox used. Find out more about setting the from and sender "
209
+ "properties of a message."
210
+ )
211
+ },
212
+ "has_attachments": {
213
+ "description": (
214
+ "Indicates whether the message has attachments. This property doesn't "
215
+ "include inline attachments, so if a message contains only inline "
216
+ "attachments, this property is false. To verify the existence of inline "
217
+ "attachments, parse the body property to look for a `src` attribute, "
218
+ 'such as `<IMG src="cid:image001.jpg@01D26CD8.6C05F070">`.'
219
+ )
220
+ },
221
+ "id": {
222
+ "description": (
223
+ "Unique identifier for the message. By default, this value changes "
224
+ "when the item is moved from one container (such as a folder or "
225
+ "calendar) to another. To change this behavior, use the `Prefer: "
226
+ 'IdType="ImmutableId"` header. See Get immutable identifiers for '
227
+ "Outlook resources for more information. Read-only."
228
+ )
229
+ },
230
+ "importance": {
231
+ "description": (
232
+ "The importance of the message. The possible values are: `low`, "
233
+ "`normal`, and `high`."
234
+ )
235
+ },
236
+ "inference_classification": {
237
+ "description": (
238
+ "The classification of the message for the user, based on inferred "
239
+ "relevance or importance, or on an explicit override. The possible "
240
+ "values are: `focused` or `other`."
241
+ )
242
+ },
243
+ "internet_message_headers": {
244
+ "description": (
245
+ "A collection of message headers defined by RFC5322. The set includes "
246
+ "message headers indicating the network path taken by a message from "
247
+ "the sender to the recipient. It can also contain custom message "
248
+ "headers that hold app data for the message. Requires `$select` to "
249
+ "retrieve. Read-only."
250
+ )
251
+ },
252
+ "internet_message_id": {
253
+ "description": "The message ID in the format specified by RFC2822."
254
+ },
255
+ "is_delivery_receipt_requested": {
256
+ "description": "Indicates whether a read receipt is requested for the message."
257
+ },
258
+ "is_draft": {
259
+ "description": (
260
+ "Indicates whether the message is a draft. A message is a draft if it "
261
+ "hasn't been sent yet."
262
+ )
263
+ },
264
+ "is_read": {"description": "Indicates whether the message has been read."},
265
+ "is_read_receipt_requested": {
266
+ "description": "Indicates whether a read receipt is requested for the message."
267
+ },
268
+ "last_modified_date_time": {
269
+ "description": (
270
+ "The date and time the message was last changed. The date and time "
271
+ "information uses ISO 8601 format and is always in UTC time. For "
272
+ "example, midnight UTC on Jan 1, 2014 is `2014-01-01T00:00:00Z`."
273
+ )
274
+ },
275
+ "multi_value_extended_properties": {
276
+ "description": (
277
+ "The collection of multi-value extended properties defined for the "
278
+ "message. Nullable."
279
+ )
280
+ },
281
+ "parent_folder_id": {
282
+ "description": "The unique identifier for the message's parent mailFolder."
283
+ },
284
+ "received_date_time": {
285
+ "description": (
286
+ "The date and time the message was received. The date and time "
287
+ "information uses ISO 8601 format and is always in UTC time. For "
288
+ "example, midnight UTC on Jan 1, 2014 is `2014-01-01T00:00:00Z`."
289
+ )
290
+ },
291
+ "reply_to": {"description": "The email addresses to use when replying."},
292
+ "sender": {
293
+ "description": (
294
+ "The account that is used to generate the message. In most cases, this "
295
+ "value is the same as the from property. You can set this property to "
296
+ "a different value when sending a message from a shared mailbox, for a "
297
+ "shared calendar, or as a delegate. In any case, the value must "
298
+ "correspond to the actual mailbox used. Find out more about setting "
299
+ "the from and sender properties of a message."
300
+ )
301
+ },
302
+ "sent_date_time": {
303
+ "description": (
304
+ "The date and time the message was sent. The date and time information "
305
+ "uses ISO 8601 format and is always in UTC time. For example, midnight "
306
+ "UTC on Jan 1, 2014 is `2014-01-01T00:00:00Z`."
307
+ )
308
+ },
309
+ "single_value_extended_properties": {
310
+ "description": (
311
+ "The collection of single-value extended properties defined for the "
312
+ "message. Nullable."
313
+ )
314
+ },
315
+ "subject": {"description": "The subject of the message."},
316
+ "to_recipients": {"description": "The To: recipients for the message."},
317
+ "unique_body": {
318
+ "description": (
319
+ "The part of the body of the message that is unique to the current "
320
+ "message. uniqueBody is not returned by default but can be retrieved "
321
+ "for a given message by use of the `?$select=uniqueBody` query. It can "
322
+ "be in HTML or text format."
323
+ )
324
+ },
325
+ "web_link": {
326
+ "description": (
327
+ "The URL to open the message in Outlook on the web. You can append an "
328
+ "`ispopout` argument to the end of the URL to change how the message "
329
+ "is displayed. If `ispopout` is not present or if it is set to `1`, "
330
+ "then the message is shown in a popout window. If `ispopout` is set to "
331
+ "`0`, the browser shows the message in the Outlook on the web review "
332
+ "pane. The message opens in the browser if you are signed in to your "
333
+ "mailbox via Outlook on the web. You are prompted to sign in if you "
334
+ "are not already signed in with the browser. This URL cannot be "
335
+ "accessed from within an iFrame. NOTE: When using this URL to access a "
336
+ "message from a mailbox with delegate permissions, both the signed-in "
337
+ "user and the target mailbox must be in the same database region. For "
338
+ "example, an error is returned when a user with a mailbox in the EUR "
339
+ "(Europe) region attempts to access messages from a mailbox in the NAM "
340
+ "(North America) region."
341
+ )
342
+ },
343
+ }
344
+
345
+ ATTACHMENT_CONTENT_COLUMN_DESCRIPTIONS = {
346
+ "attachment_id": {
347
+ "description": "Microsoft Graph attachment id (per-message stable identifier)."
348
+ },
349
+ "name": {"description": "The attachment's file name."},
350
+ "content_type": {"description": "The MIME type."},
351
+ "size": {"description": "The length of the attachment in bytes."},
352
+ "is_inline": {
353
+ "description": (
354
+ "`true` if the attachment is referenced inline from the message body "
355
+ "(e.g. a `cid:`-referenced image)."
356
+ )
357
+ },
358
+ "content_id": {
359
+ "description": (
360
+ 'Microsoft Graph contentId, used to match `<img src="cid:{contentId}">` '
361
+ "tags in the message body. Only set for fileAttachment subtypes."
362
+ )
363
+ },
364
+ "content_location": {"description": "Microsoft Graph contentLocation. Often NULL."},
365
+ "last_modified_date_time": {
366
+ "description": (
367
+ "Per-attachment last-modified timestamp from Graph. Same source as "
368
+ "_ctx_source_updated_at."
369
+ )
370
+ },
371
+ "file_path": {
372
+ "description": (
373
+ "Local on-disk path written by the content materialization job. "
374
+ "Always set — this table only contains rows for attachments with "
375
+ "materialized bytes."
376
+ )
377
+ },
378
+ }