extract-msg 0.48.2__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.
Files changed (95) hide show
  1. extract_msg/__init__.py +71 -0
  2. extract_msg/__main__.py +115 -0
  3. extract_msg/_rtf/__init__.py +21 -0
  4. extract_msg/_rtf/create_doc.py +29 -0
  5. extract_msg/_rtf/inject_rtf.py +174 -0
  6. extract_msg/_rtf/token.py +35 -0
  7. extract_msg/_rtf/tokenize_rtf.py +253 -0
  8. extract_msg/attachments/__init__.py +144 -0
  9. extract_msg/attachments/attachment.py +188 -0
  10. extract_msg/attachments/attachment_base.py +730 -0
  11. extract_msg/attachments/broken_att.py +42 -0
  12. extract_msg/attachments/custom_att.py +173 -0
  13. extract_msg/attachments/custom_att_handler/__init__.py +80 -0
  14. extract_msg/attachments/custom_att_handler/custom_handler.py +134 -0
  15. extract_msg/attachments/custom_att_handler/lnk_obj_att.py +109 -0
  16. extract_msg/attachments/custom_att_handler/outlook_image_dib.py +141 -0
  17. extract_msg/attachments/emb_msg_att.py +134 -0
  18. extract_msg/attachments/signed_att.py +234 -0
  19. extract_msg/attachments/unsupported_att.py +40 -0
  20. extract_msg/attachments/web_att.py +74 -0
  21. extract_msg/constants/__init__.py +294 -0
  22. extract_msg/constants/ps.py +46 -0
  23. extract_msg/constants/re.py +42 -0
  24. extract_msg/constants/st.py +110 -0
  25. extract_msg/data/logging-config/logging-nt.json +64 -0
  26. extract_msg/data/logging-config/logging-posix.json +64 -0
  27. extract_msg/encoding/__init__.py +277 -0
  28. extract_msg/encoding/_dt/__init__.py +4 -0
  29. extract_msg/encoding/_dt/_mac_ce.py +8 -0
  30. extract_msg/encoding/_dt/_mac_cyrillic.py +8 -0
  31. extract_msg/encoding/_dt/_mac_greek.py +8 -0
  32. extract_msg/encoding/_dt/_mac_iceland.py +8 -0
  33. extract_msg/encoding/_dt/_mac_turkish.py +8 -0
  34. extract_msg/encoding/_dt/_win874_dec.py +8 -0
  35. extract_msg/encoding/_dt/_win950_dec.py +6 -0
  36. extract_msg/encoding/utils.py +329 -0
  37. extract_msg/enums.py +2067 -0
  38. extract_msg/exceptions.py +170 -0
  39. extract_msg/msg_classes/__init__.py +49 -0
  40. extract_msg/msg_classes/appointment.py +195 -0
  41. extract_msg/msg_classes/calendar.py +92 -0
  42. extract_msg/msg_classes/calendar_base.py +496 -0
  43. extract_msg/msg_classes/contact.py +1335 -0
  44. extract_msg/msg_classes/journal.py +131 -0
  45. extract_msg/msg_classes/meeting_cancellation.py +108 -0
  46. extract_msg/msg_classes/meeting_exception.py +55 -0
  47. extract_msg/msg_classes/meeting_forward.py +115 -0
  48. extract_msg/msg_classes/meeting_related.py +65 -0
  49. extract_msg/msg_classes/meeting_request.py +189 -0
  50. extract_msg/msg_classes/meeting_response.py +74 -0
  51. extract_msg/msg_classes/message.py +12 -0
  52. extract_msg/msg_classes/message_base.py +1382 -0
  53. extract_msg/msg_classes/message_signed.py +17 -0
  54. extract_msg/msg_classes/message_signed_base.py +168 -0
  55. extract_msg/msg_classes/msg.py +1172 -0
  56. extract_msg/msg_classes/post.py +53 -0
  57. extract_msg/msg_classes/sticky_note.py +67 -0
  58. extract_msg/msg_classes/task.py +363 -0
  59. extract_msg/msg_classes/task_request.py +86 -0
  60. extract_msg/null_date.py +34 -0
  61. extract_msg/ole_writer.py +1086 -0
  62. extract_msg/open_msg.py +204 -0
  63. extract_msg/properties/__init__.py +23 -0
  64. extract_msg/properties/named.py +481 -0
  65. extract_msg/properties/prop.py +506 -0
  66. extract_msg/properties/properties_store.py +476 -0
  67. extract_msg/py.typed +0 -0
  68. extract_msg/recipient.py +362 -0
  69. extract_msg/structures/__init__.py +34 -0
  70. extract_msg/structures/_helpers.py +345 -0
  71. extract_msg/structures/business_card.py +422 -0
  72. extract_msg/structures/cfoas.py +108 -0
  73. extract_msg/structures/contact_link_entry.py +51 -0
  74. extract_msg/structures/dev_mode_a.py +621 -0
  75. extract_msg/structures/dv_target_device.py +168 -0
  76. extract_msg/structures/entry_id.py +697 -0
  77. extract_msg/structures/misc_id.py +222 -0
  78. extract_msg/structures/mon_stream.py +55 -0
  79. extract_msg/structures/odt.py +74 -0
  80. extract_msg/structures/ole_pres.py +302 -0
  81. extract_msg/structures/ole_stream_struct.py +117 -0
  82. extract_msg/structures/recurrence_pattern.py +199 -0
  83. extract_msg/structures/report_tag.py +127 -0
  84. extract_msg/structures/system_time.py +158 -0
  85. extract_msg/structures/time_zone_definition.py +111 -0
  86. extract_msg/structures/time_zone_struct.py +102 -0
  87. extract_msg/structures/toc_entry.py +137 -0
  88. extract_msg/structures/tz_rule.py +148 -0
  89. extract_msg/utils.py +1270 -0
  90. extract_msg-0.48.2.dist-info/LICENSE.txt +674 -0
  91. extract_msg-0.48.2.dist-info/METADATA +321 -0
  92. extract_msg-0.48.2.dist-info/RECORD +95 -0
  93. extract_msg-0.48.2.dist-info/WHEEL +5 -0
  94. extract_msg-0.48.2.dist-info/entry_points.txt +2 -0
  95. extract_msg-0.48.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'BusinessCardDisplayDefinition',
6
+ 'FieldInfo',
7
+ ]
8
+
9
+
10
+ import logging
11
+ import struct
12
+
13
+ from typing import List, Optional, Tuple
14
+
15
+ from ._helpers import BytesReader
16
+ from .. import constants
17
+ from ..enums import (
18
+ BCImageAlignment, BCImageSource, BCLabelFormat, BCTemplateID,
19
+ BCTextFormat
20
+ )
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+ logger.addHandler(logging.NullHandler())
25
+
26
+
27
+ class BusinessCardDisplayDefinition:
28
+ """
29
+ Data structure for PidLidBusinessCardDisplayDefinition.
30
+
31
+ Contains information used to contruct a business card for a contact.
32
+ """
33
+
34
+ def __init__(self, data: bytes):
35
+ reader = BytesReader(data)
36
+ unpacked = constants.st.ST_BC_HEAD.unpack(reader.read(13))
37
+ # Because doc says it must be ignored, we don't check the reserved here.
38
+ reader.read(4)
39
+ self.__majorVersion = unpacked[0]
40
+ if self.__majorVersion < 3:
41
+ raise ValueError('Major version was less than 3.')
42
+ self.__minorVersion = unpacked[1]
43
+ self.__templateID = BCTemplateID(unpacked[2])
44
+ countOfFields = unpacked[3]
45
+ if unpacked[4] != 16:
46
+ raise ValueError('Value of FieldInfoSize was not 16.')
47
+ extraInfoSize = unpacked[5]
48
+ self.__imageAlignment = BCImageAlignment(unpacked[6])
49
+ self.__imageSource = BCImageSource(min(unpacked[7], 1))
50
+ self.__backgroundColor = unpacked[8:11]
51
+ self.__imageArea = unpacked[11]
52
+ extraInfoField = data[17 + 16 * countOfFields:]
53
+ extraInfoField = data[:extraInfoSize]
54
+ self.__fields = [FieldInfo(reader.read(16), extraInfoField)
55
+ for _ in range(countOfFields)]
56
+
57
+ def __bytes__(self) -> bytes:
58
+ return self.toBytes()
59
+
60
+ def toBytes(self) -> bytes:
61
+ # Pregenerate the extraInfo field.
62
+ offsets = []
63
+ extraInfo = b''
64
+ for info in self.__fields:
65
+ if info.labelText:
66
+ offsets.append(len(extraInfo))
67
+ extraInfo += info.labelText.encode('utf-16-le') + b'\x00\x00'
68
+ else:
69
+ offsets.append(0xFFFE)
70
+
71
+ if len(extraInfo) > 255:
72
+ raise ValueError('Label data can only be 127 characters total, including null characters.')
73
+
74
+ # Now that we have what we need, pack the header.
75
+ ret = constants.st.ST_BC_HEAD.pack(
76
+ self.__majorVersion,
77
+ self.__minorVersion,
78
+ self.__templateID,
79
+ len(self.__fields),
80
+ 16,
81
+ len(extraInfo),
82
+ self.__imageAlignment,
83
+ self.__imageSource,
84
+ *self.__backgroundColor,
85
+ self.__imageArea
86
+ )
87
+
88
+ # Add the reserved.
89
+ ret += b'\x00\x00\x00\x00'
90
+
91
+ # Add each FieldInfo structure.
92
+ for index, info in enumerate(self.__fields):
93
+ ret += info.toBytes(offsets[index])
94
+
95
+ return ret + extraInfo
96
+
97
+ @property
98
+ def backgroundColor(self) -> Tuple[int, int, int]:
99
+ """
100
+ A tuple of the RGB value of the color of the background.
101
+ """
102
+ return self.__backgroundColor
103
+
104
+ @backgroundColor.setter
105
+ def backgroundColor(self, value: Tuple[int, int, int]) -> None:
106
+ if not isinstance(value, tuple) or len(value) != 3:
107
+ raise TypeError(':property backgroundColor: MUST be a tuple of 3 ints.')
108
+ # Quickly try to pack the ints and raise a value error if that fails.
109
+ try:
110
+ constants.st.ST_RGB(*value)
111
+ except struct.error:
112
+ raise ValueError('Value for :property backgroundColor: not in range.')
113
+
114
+ self.__backgroundColor = value
115
+
116
+ @property
117
+ def fields(self) -> List[FieldInfo]:
118
+ """
119
+ The field info structures.
120
+ """
121
+ return self.__fields
122
+
123
+ @property
124
+ def imageAlignment(self) -> BCImageAlignment:
125
+ """
126
+ The alignment of the image within the image area.
127
+
128
+ Ignored if card is text only.
129
+ """
130
+ return self.__imageAlignment
131
+
132
+ @imageAlignment.setter
133
+ def imageAlignment(self, value: BCImageAlignment) -> None:
134
+ if not isinstance(value, BCImageAlignment):
135
+ raise TypeError(':property imageAlignment: MUST be an instance of BCImageAlignment.')
136
+
137
+ self.__imageAlignment = value
138
+
139
+ @property
140
+ def imageArea(self) -> int:
141
+ """
142
+ An integer that specified the percent of space that the image will
143
+ occupy on the business card.
144
+
145
+ Should be between 4 and 50.
146
+ """
147
+ return self.__imageArea
148
+
149
+ @imageArea.setter
150
+ def imageArea(self, value: int) -> None:
151
+ if not isinstance(value, int):
152
+ raise TypeError(':property imageArea: MUST be an int.')
153
+ if value < 0:
154
+ raise ValueError(':property imageArea: MUST be positive.')
155
+ if value > 0xFF:
156
+ raise ValueError(':property imageArea: MUST be less than 0x100.')
157
+
158
+ if value < 4 or value > 50:
159
+ logger.warning(f'Business card image area was set to a value outside of suggested range of [4, 50] (got {value}).')
160
+
161
+ self.__imageArea = value
162
+
163
+ @property
164
+ def imageSource(self) -> BCImageSource:
165
+ """
166
+ The source of the image.
167
+ """
168
+ return self.__imageSource
169
+
170
+ @imageSource.setter
171
+ def imageSource(self, value: BCImageSource) -> None:
172
+ if not isinstance(value, BCImageSource):
173
+ raise TypeError(':property imageSource: MUST be an instance of BCImageSource.')
174
+
175
+ self.__imageSource = value
176
+
177
+ @property
178
+ def majorVersion(self) -> int:
179
+ """
180
+ An 8-bit value that specified the major version number.
181
+
182
+ Must be 3 or greater.
183
+ """
184
+ return self.__majorVersion
185
+
186
+ @majorVersion.setter
187
+ def majorVersion(self, value: int) -> None:
188
+ if not isinstance(value, int):
189
+ raise TypeError(':property majorVersion: MUST be an int.')
190
+ if value < 3:
191
+ raise ValueError(':property majorVersion: MUST be 3 or greater.')
192
+ if value > 0xFF:
193
+ raise ValueError(':property majorVersion: MUST be less than 0x100.')
194
+
195
+ self.__majorVersion = value
196
+
197
+ @property
198
+ def minorVersion(self) -> int:
199
+ """
200
+ An 8-bit value that specifies the minor version number.
201
+
202
+ SHOULD be set to 0.
203
+ """
204
+ return self.__minorVersion
205
+
206
+ @minorVersion.setter
207
+ def minorVersion(self, value: int) -> None:
208
+ if not isinstance(value, int):
209
+ raise TypeError(':property minorVersion: MUST be an int.')
210
+ if value < 0:
211
+ raise ValueError(':property minorVersion: MUST be positive.')
212
+ if value > 0xFF:
213
+ raise ValueError(':property minorVersion: MUST be less than 0x100.')
214
+
215
+ if value != 0:
216
+ logger.warning('Business card minor version set to non-zero value.')
217
+
218
+ self.__minorVersion = value
219
+
220
+ @property
221
+ def templateID(self) -> BCTemplateID:
222
+ """
223
+ The layout of the business card.
224
+ """
225
+ return self.__templateID
226
+
227
+ @templateID.setter
228
+ def templateID(self, value: BCTemplateID):
229
+ if not isinstance(value, BCTemplateID):
230
+ raise TypeError(':property templateID: MUST be an instance of BCTemplateID.')
231
+
232
+ self.__templateID = value
233
+
234
+
235
+
236
+ class FieldInfo:
237
+ def __init__(self, data: Optional[bytes] = None, extraInfo: Optional[bytes] = None):
238
+ if not data:
239
+ self.__textPropertyID = 0
240
+ self.__textFormat = BCTextFormat.DEFAULT
241
+ self.__labelFormat = BCLabelFormat.NO_LABEL
242
+ self.__fontSize = 0
243
+ self.__labelText = None
244
+ self.__valueFontColor = (0, 0, 0)
245
+ self.__labelFontColor = (0, 0, 0)
246
+ return
247
+
248
+ if extraInfo is None:
249
+ raise ValueError(':param extraInfo: MUST NOT be None if data is provided.')
250
+
251
+ unpacked = constants.st.ST_BC_FIELD_INFO.unpack(data)
252
+ self.__textPropertyID = unpacked[0]
253
+ self.__textFormat = BCTextFormat(unpacked[1])
254
+ self.__labelFormat = BCLabelFormat(unpacked[2])
255
+ self.__fontSize = unpacked[3]
256
+ self.__labelText = None if unpacked[4] == 0xFFFE else BytesReader(extraInfo[unpacked[4]:]).readUtf16String()
257
+ self.__valueFontColor = unpacked[5:8]
258
+ self.__labelFontColor = unpacked[8:11]
259
+
260
+ def toBytes(self, offset: int) -> bytes:
261
+ """
262
+ Converts to bytes using the offset into the ExtraInfo field.
263
+
264
+ :raise ValueError: The offset was out of range.
265
+ """
266
+ if offset < 0 or offset > 0xFFFF:
267
+ raise ValueError('Offset is out of range.')
268
+
269
+ if not self.__labelText:
270
+ offset = 0xFFFE
271
+
272
+ return constants.st.ST_BC_FIELD_INFO.pack(
273
+ self.__textPropertyID,
274
+ self.__textFormat,
275
+ self.__labelFormat,
276
+ self.__fontSize,
277
+ offset,
278
+ *self.__valueFontColor,
279
+ *self.__labelFontColor
280
+ )
281
+
282
+ @property
283
+ def fontSize(self) -> int:
284
+ """
285
+ An integer that specifies the font size, in points, of the text field.
286
+
287
+ MUST be between 3 and 32, or MUST be 0 if the text field is displayed as
288
+ an empty line.
289
+ """
290
+ return self.__fontSize
291
+
292
+ @fontSize.setter
293
+ def fontSize(self, value: int) -> None:
294
+ if not isinstance(value, int):
295
+ raise TypeError(':property fontSize: MUST be an int.')
296
+
297
+ # Unsure if this is meant to be inclusive or exclusive.
298
+ if value == 0 or 3 <= value <= 32:
299
+ self.__fontSize = value
300
+ else:
301
+ raise ValueError(':property fontSize: MUST be 0 or between 3 and 32, inclusive.')
302
+
303
+ @property
304
+ def labelFontColor(self) -> Tuple[int, int, int]:
305
+ """
306
+ A tuple of the RGB value of the color of the label.
307
+
308
+ Each channel is a number in range [0, 256).
309
+ """
310
+ return self.__labelFontColor
311
+
312
+ @labelFontColor.setter
313
+ def labelFontColor(self, value: Tuple[int, int, int]) -> None:
314
+ if not isinstance(value, tuple) or len(value) != 3:
315
+ raise TypeError(':property labelFontColor: MUST be a tuple of 3 ints.')
316
+ # Quickly try to pack the ints and raise a value error if that fails.
317
+ try:
318
+ constants.st.ST_RGB(*value)
319
+ except struct.error:
320
+ raise ValueError('Value for :property labelFontColor: not in range.')
321
+
322
+ self.__labelFontColor = value
323
+
324
+ @property
325
+ def labelFormat(self) -> BCLabelFormat:
326
+ """
327
+ The format to use for the label.
328
+ """
329
+ return self.__labelFormat
330
+
331
+ @labelFormat.setter
332
+ def labelFormat(self, value: BCLabelFormat) -> None:
333
+ if not isinstance(value, BCLabelFormat):
334
+ raise TypeError(':property labelFormat: MUST be an instance of BCLabelFormat.')
335
+ # Check mutually exclusive flags.
336
+ if BCLabelFormat.ALIGN_LEFT in value and BCLabelFormat.ALIGN_RIGHT in value:
337
+ raise ValueError('BCLabelFormat.ALIGN_LEFT and BCLabelFormat.ALIGN_RIGHT are mutually exclusive.')
338
+
339
+ if value and BCLabelFormat.ALIGN_LEFT not in value and BCLabelFormat.ALIGN_RIGHT not in value:
340
+ raise ValueError(':property labelFormat: MUST have the ALIGN_LEFT or ALIGN_RIGHT bit set if any other bits are set.')
341
+
342
+ if BCLabelFormat > 0x110:
343
+ raise ValueError('Unknown bits set for :property labelFormat:.')
344
+
345
+ self.__textPropertyID = value
346
+
347
+ @property
348
+ def labelText(self) -> Optional[str]:
349
+ """
350
+ The text of the label, if it exists.
351
+ """
352
+ return self.__labelText
353
+
354
+ @labelText.setter
355
+ def labelText(self, value: Optional[str]) -> None:
356
+ if not value:
357
+ self.__labelText = None
358
+ elif not isinstance(value, str):
359
+ raise TypeError(':property labelText: MUST be a str or None.')
360
+
361
+ self.__labelText = value
362
+
363
+ @property
364
+ def textFormat(self) -> BCTextFormat:
365
+ """
366
+ An enum value representing the formatting to use for the text.
367
+ """
368
+ return self.__textFormat
369
+
370
+ @textFormat.setter
371
+ def textFormat(self, value: BCTextFormat) -> None:
372
+ if not isinstance(value, BCTextFormat):
373
+ raise TypeError(':property textFormat: MUST be an instance of BCTextFormat.')
374
+ # Check mutually exclusive flags.
375
+ if BCTextFormat.CENTER in value and BCTextFormat.RIGHT in value:
376
+ raise ValueError('BCTextFormat.RIGHT and BCTextFormat.CENTER are mutually exclusive.')
377
+
378
+ if BCTextFormat > 0x101111:
379
+ raise ValueError('Unknown bits set for :property textFormat:.')
380
+
381
+ self.__textPropertyID = value
382
+
383
+ @property
384
+ def textPropertyID(self) -> int:
385
+ """
386
+ The property to be used for the text field.
387
+
388
+ If the value is 0, it represents an empty field.
389
+ """
390
+ return self.__textPropertyID
391
+
392
+ @textPropertyID.setter
393
+ def textPropertyID(self, value: int) -> None:
394
+ if not isinstance(value, int):
395
+ raise TypeError(':property textPropertyID: MUST be an int.')
396
+ if value < 0:
397
+ raise ValueError(':property textPropertyID: MUST be positive.')
398
+ if value > 0xFFFF:
399
+ raise ValueError(':property textPropertyID: MUST NOT be greater than 0xFFFF.')
400
+
401
+ self.__textPropertyID = value
402
+
403
+ @property
404
+ def valueFontColor(self) -> Tuple[int, int, int]:
405
+ """
406
+ A tuple of the RGB value of the color of the text field.
407
+
408
+ Each channel is a number in range [0, 256).
409
+ """
410
+ return self.__valueFontColor
411
+
412
+ @valueFontColor.setter
413
+ def valueFontColor(self, value: Tuple[int, int, int]) -> None:
414
+ if not isinstance(value, tuple) or len(value) != 3:
415
+ raise TypeError(':property valueFontColor: MUST be a tuple of 3 ints.')
416
+ # Quickly try to pack the ints and raise a value error if that fails.
417
+ try:
418
+ constants.st.ST_RGB(*value)
419
+ except struct.error:
420
+ raise ValueError('Value for :property valueFontColor: not in range.')
421
+
422
+ self.__valueFontColor = value
@@ -0,0 +1,108 @@
1
+ __all__ = [
2
+ 'ClipboardFormatOrAnsiString',
3
+ ]
4
+
5
+
6
+ from typing import Optional, Union
7
+
8
+ from .. import constants
9
+ from ._helpers import BytesReader
10
+ from ..enums import ClipboardFormat
11
+
12
+
13
+ class ClipboardFormatOrAnsiString:
14
+ def __init__(self, reader: Optional[Union[bytes, BytesReader]] = None):
15
+ if reader is None:
16
+ self.__markerOrLength = 0
17
+ self.__clipboardFormat = None
18
+ self.__ansiString = None
19
+ return
20
+
21
+ if isinstance(reader, bytes):
22
+ reader = BytesReader(reader)
23
+
24
+ self.__markerOrLength = reader.readUnsignedInt()
25
+ if self.__markerOrLength > 0xFFFFFFFD:
26
+ self.__ansiString = None
27
+ self.__clipboardFormat = ClipboardFormat(reader.readUnsignedInt())
28
+ elif self.__markerOrLength > 0:
29
+ self.__ansiString = reader.read(self.__markerOrLength)
30
+ self.__clipboardFormat = None
31
+ else:
32
+ self.__ansiString = None
33
+ self.__clipboardFormat = None
34
+
35
+ def __bytes__(self) -> bytes:
36
+ return self.toBytes()
37
+
38
+ def toBytes(self) -> bytes:
39
+ ret = constants.st.ST_LE_UI32.pack(self.markerOrLength)
40
+ if self.markerOrLength > 0xFFFFFFFD:
41
+ ret += constants.st.ST_LE_UI32.pack(self.clipboardFormat)
42
+ elif self.markerOrLength > 0:
43
+ ret += self.__ansiString
44
+ return ret
45
+
46
+ @property
47
+ def ansiString(self) -> Optional[bytes]:
48
+ """
49
+ The null-terminated ANSI string, as bytes, of the name of a registered
50
+ clipboard format. Only set if markerOrLength is not ``0x00000000``,
51
+ ``0xFFFFFFFE``, or ``0xFFFFFFFF``.
52
+
53
+ Setting this will modify the markerOrLength field automatically.
54
+ """
55
+ return self.__ansiString
56
+
57
+ @ansiString.setter
58
+ def ansiString(self, val: bytes) -> None:
59
+ if not val:
60
+ raise ValueError('Cannot set :property ansiString: to None or empty bytes.')
61
+
62
+ self.__ansiString = val
63
+
64
+ @property
65
+ def clipboardFormat(self) -> Optional[ClipboardFormat]:
66
+ """
67
+ The clipboard format, if any.
68
+
69
+ To set this, make sure that :property markerOrLength: is ``0xFFFFFFFE``
70
+ or ``0xFFFFFFFF`` *before* setting.
71
+ """
72
+ return self.__clipboardFormat
73
+
74
+ @clipboardFormat.setter
75
+ def clipboardFormat(self, val: ClipboardFormat) -> None:
76
+ if not val:
77
+ raise ValueError('Cannot set clipboard format to None.')
78
+ if self.markerOrLength < 0xFFFFFFFE:
79
+ raise ValueError('Cannot set the clipboard format while the marker or length is not 0xFFFFFFFE or 0xFFFFFFFF')
80
+ self.__clipboardFormat = val
81
+
82
+ @property
83
+ def markerOrLength(self) -> int:
84
+ """
85
+ If set the 0x00000000, then neither the format property nor the
86
+ ansiString property will be set. If it is 0xFFFFFFFF or 0xFFFFFFFE, then
87
+ the clipboardFormat property will be set. Otherwise, the ansiString
88
+ property
89
+ will be set.
90
+ """
91
+ return self.__markerOrLength
92
+
93
+ @markerOrLength.setter
94
+ def markerOrLength(self, val: int) -> None:
95
+ if val < 0:
96
+ raise ValueError(':property markerOrLength: must be a positive integer.')
97
+ if val > 0xFFFFFFFF:
98
+ raise ValueError(':property markerOrLength: cannot be greater than 0xFFFFFFFF')
99
+
100
+ if val == 0:
101
+ self.__ansiString = None
102
+ self.__clipboardFormat = None
103
+ elif val > 0xFFFFFFFD:
104
+ self.__ansiString = None
105
+ self.__clipboardFormat = ClipboardFormat.CF_BITMAP
106
+ else:
107
+ raise ValueError('Cannot set :property markerOrLength: to a length value. Set :property ansiString: instead.')
108
+ self.__markerOrLength = val
@@ -0,0 +1,51 @@
1
+ __all__ = [
2
+ 'ContactLinkEntry',
3
+ ]
4
+
5
+
6
+ from typing import List
7
+
8
+ from ._helpers import BytesReader
9
+ from ..constants import st
10
+ from .entry_id import EntryID
11
+
12
+
13
+ class ContactLinkEntry:
14
+ entries: List[EntryID]
15
+
16
+ def __init__(self, data: bytes):
17
+ # My experience with this data almost entirely doesn't match the
18
+ # documentation, so I'm just going to do what I see and not what I'm
19
+ # told.
20
+ reader = BytesReader(data)
21
+ count = reader.readUnsignedInt()
22
+ # Ignore this field.
23
+ reader.read(4)
24
+ self.entries = []
25
+ for _ in range(count):
26
+ size = reader.readUnsignedInt()
27
+ self.entries.append(EntryID.autoCreate(reader.read(size)))
28
+ if (size & 3) != 0:
29
+ reader.read(4 - (size & 3))
30
+
31
+ def __bytes__(self) -> bytes:
32
+ return self.toBytes()
33
+
34
+ def toBytes(self) -> bytes:
35
+ ret = st.ST_LE_UI32.pack(len(self.entries))
36
+
37
+ # Need to handle the data before hand.
38
+ data = b''
39
+ for entry in self.entries:
40
+ entryData = entry.toBytes()
41
+ # Size goes before data.
42
+ data += st.ST_LE_UI32.pack(edLen := len(entryData))
43
+ data += entryData
44
+ # Handle padding.
45
+ if edLen & 3:
46
+ data += b'\x00' * (4 - edLen)
47
+
48
+ ret += st.ST_LE_UI32.pack(len(data))
49
+ ret += data
50
+
51
+ return ret