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,506 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ # Classes:
6
+ 'FixedLengthProp',
7
+ 'PropBase',
8
+ 'VariableLengthProp',
9
+
10
+ # Functions:
11
+ 'createProp',
12
+ 'createNewProp',
13
+ ]
14
+
15
+
16
+ import abc
17
+ import datetime
18
+ import decimal
19
+ import logging
20
+
21
+ from typing import Any, Dict, Type
22
+
23
+ from .. import constants
24
+ from ..enums import ErrorCode, ErrorCodeType, PropertyFlags
25
+ from ..utils import filetimeToDatetime
26
+
27
+
28
+ logger = logging.getLogger(__name__)
29
+ logger.addHandler(logging.NullHandler())
30
+
31
+ # Define default values to use when creating each prop type. Only define a value
32
+ # if it would not be all null bytes.
33
+ _DEFAULT_PROP_VALS: Dict[str, bytes] = {
34
+ '000D': b'\x00\x00\x00\x00\xFF\xFF\xFF\xFF\x00\x00\x00\x00',
35
+ '001E': b'\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00',
36
+ '001F': b'\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00',
37
+ '0048': b'\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00',
38
+ '0000': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
39
+ '0000': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
40
+ '0000': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
41
+ '0000': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
42
+ '0000': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
43
+ }
44
+
45
+
46
+ def createNewProp(name: str):
47
+ """
48
+ Creates a blank property using the specified name.
49
+
50
+ :param name: An 8 character hex string containing the property ID and type.
51
+
52
+ :raises TypeError: A type other than a str was given.
53
+ :raises ValueError: The string was not 8 hex characters.
54
+ :raises ValueError: An invalid property type was given.
55
+ """
56
+ if not isinstance(name, str):
57
+ raise TypeError(':param name: MUST be a str.')
58
+ if len(name) != 8:
59
+ raise ValueError(':param name: MUST be 8 characters.')
60
+
61
+ propVal = bytes.fromhex(name)[::-1]
62
+ propVal += _DEFAULT_PROP_VALS.get(name[:4], b'\x00' * 12)
63
+ return createProp(propVal)
64
+
65
+
66
+ def createProp(data: bytes) -> PropBase:
67
+ """
68
+ Creates an instance of PropBase from the specified bytes.
69
+
70
+ If the prop type is not recognized, a VariableLengthProp will be created.
71
+ """
72
+ temp = constants.st.ST_PROP_BASE.unpack(data[:8])[0]
73
+ if temp in constants.FIXED_LENGTH_PROPS:
74
+ return FixedLengthProp(data)
75
+ else:
76
+ if temp not in constants.VARIABLE_LENGTH_PROPS:
77
+ # DEBUG.
78
+ logger.warning(f'Unknown property type: {temp:04X}')
79
+ return VariableLengthProp(data)
80
+
81
+
82
+ class PropBase(abc.ABC):
83
+ """
84
+ Base class for Prop instances.
85
+ """
86
+
87
+ def __init__(self, data: bytes):
88
+ self.__name = data[3::-1].hex().upper()
89
+ self.__type, self.__pID, flags = constants.st.ST_PROP_BASE.unpack(data[:8])
90
+ self.__flags = PropertyFlags(flags)
91
+
92
+ def __bytes__(self) -> bytes:
93
+ return self.toBytes()
94
+
95
+ @abc.abstractmethod
96
+ def toBytes(self) -> bytes:
97
+ """
98
+ Converts the property into a string of 16 bytes.
99
+ """
100
+
101
+ @property
102
+ def flags(self) -> PropertyFlags:
103
+ """
104
+ Integer that contains property flags.
105
+ """
106
+ return self.__flags
107
+
108
+ @flags.setter
109
+ def flags(self, value: PropertyFlags):
110
+ if not isinstance(value, PropertyFlags):
111
+ raise TypeError(':property flags: MUST be an instance of PropertyFlags.')
112
+
113
+ self.__flags = value
114
+
115
+ @property
116
+ def name(self) -> str:
117
+ """
118
+ Hexadecimal representation of the property ID followed by the type.
119
+ """
120
+ return self.__name
121
+
122
+ @property
123
+ def propertyID(self) -> int:
124
+ """
125
+ The property ID for this property.
126
+ """
127
+ return self.__pID
128
+
129
+ @property
130
+ def type(self) -> int:
131
+ """
132
+ The type of property.
133
+ """
134
+ return self.__type
135
+
136
+
137
+
138
+ class FixedLengthProp(PropBase):
139
+ """
140
+ Class to contain the data for a single fixed length property.
141
+
142
+ Currently a work in progress.
143
+ """
144
+
145
+ def __init__(self, data: bytes):
146
+ super().__init__(data)
147
+ self.__value = self._parseType(self.type, data[8:], data)
148
+
149
+ def _parseType(self, _type: int, stream: bytes, raw: bytes) -> Any:
150
+ """
151
+ Converts the data in :param stream: to a much more accurate type,
152
+ specified by :param _type:, if possible.
153
+ :param stream: The data that the value is extracted from.
154
+
155
+ WARNING: Not done.
156
+ """
157
+ # WARNING Not done.
158
+ value = stream
159
+ if _type == 0x0000: # PtypUnspecified
160
+ pass
161
+ elif _type == 0x0001: # PtypNull
162
+ if value != b'\x00\x00\x00\x00\x00\x00\x00\x00':
163
+ # DEBUG.
164
+ logger.warning('Property type is PtypNull, but is not equal to 0.')
165
+ value = None
166
+ elif _type == 0x0002: # PtypInteger16
167
+ value = constants.st.ST_LE_UI16.unpack(value[:2])[0]
168
+ elif _type == 0x0003: # PtypInteger32
169
+ value = constants.st.ST_LE_UI32.unpack(value[:4])[0]
170
+ elif _type == 0x0004: # PtypFloating32
171
+ value = constants.st.ST_LE_F32.unpack(value[:4])[0]
172
+ elif _type == 0x0005: # PtypFloating64
173
+ value = constants.st.ST_LE_F64.unpack(value)[0]
174
+ elif _type == 0x0006: # PtypCurrency
175
+ value = decimal.Decimal((constants.st.ST_LE_I64.unpack(value))[0]) / 10000
176
+ elif _type == 0x0007: # PtypFloatingTime
177
+ value = constants.st.ST_LE_F64.unpack(value)[0]
178
+ return constants.PYTPFLOATINGTIME_START + datetime.timedelta(days = value)
179
+ elif _type == 0x000A: # PtypErrorCode
180
+ value = constants.st.ST_LE_UI32.unpack(value[:4])[0]
181
+ try:
182
+ value = ErrorCodeType(value)
183
+ except ValueError:
184
+ logger.warning(f'Error type found that was not from Additional Error Codes. Value was {value}. You should report this to the developers.')
185
+ # So here, the value should be from Additional Error Codes, but
186
+ # it wasn't. So we are just returning the int. However, we want
187
+ # to see if it is a normal error code.
188
+ try:
189
+ logger.warning(f'REPORT TO DEVELOPERS: Error type of {ErrorCode(value)} was found.')
190
+ except ValueError:
191
+ pass
192
+ elif _type == 0x000B: # PtypBoolean
193
+ value = constants.st.ST_LE_UI16.unpack(value[:2])[0] != 0
194
+ elif _type == 0x0014: # PtypInteger64
195
+ value = constants.st.ST_LE_UI64.unpack(value)[0]
196
+ elif _type == 0x0040: # PtypTime
197
+ rawTime = constants.st.ST_LE_UI64.unpack(value)[0]
198
+ try:
199
+ value = filetimeToDatetime(rawTime)
200
+ except ValueError:
201
+ logger.exception(raw)
202
+ return value
203
+
204
+ def toBytes(self) -> bytes:
205
+ """
206
+ Converts the property into bytes.
207
+
208
+ :raises ValueError: An issue occured where the value was not converted
209
+ to bytes.
210
+ """
211
+ # First convert the value back to bytes in some way.
212
+ value = self.value
213
+ if self.type == 0x0001:
214
+ value = b'\x00\x00\x00\x00\x00\x00\x00\x00'
215
+ elif self.type == 0x0002:
216
+ value = constants.st.ST_LE_UI16.pack(value) + b'\x00' * 6
217
+ elif self.type == 0x0003 or self.type == 0x000A:
218
+ value = constants.st.ST_LE_UI32.pack(value) + b'\x00' * 4
219
+ elif self.type == 0x0004:
220
+ value = constants.st.ST_LE_F32.pack(value) + b'\x00' * 4
221
+ elif self.type == 0x0005:
222
+ value = constants.st.ST_LE_F64.pack(value)
223
+ elif self.type == 0x0006:
224
+ value = constants.st.ST_LE_I64.pack(int(value * 10000))
225
+ elif self.type == 0x0007:
226
+ value = constants.st.ST_LE_F64.pack(
227
+ (value - constants.PYTPFLOATINGTIME_START).total_seconds() / 86400
228
+ )
229
+ elif self.type == 0x000B:
230
+ value = (b'\x01' + b'\x00' * 7) if value else (b'\x00' * 8)
231
+ elif self.type == 0x0014:
232
+ value = constants.st.ST_LE_UI64.pack(value)
233
+ elif self.type == 0x0040:
234
+ if hasattr(value, 'filetime') and value.filetime is not None:
235
+ value = value.filetime
236
+ elif isinstance(value, datetime.datetime):
237
+ try:
238
+ value = value.timestamp()
239
+ except OSError:
240
+ # Can't convert to a timestamp, so try to convert manually.
241
+ value = decimal.Decimal((value - datetime.datetime(1601, 1, 1)).total_seconds())
242
+ else:
243
+ value = decimal.Decimal(value)
244
+ value += 11644473600
245
+
246
+ # Here we now have a Decimal value. We want to convert it to an
247
+ # int representing the 100 nanosecond intervals since the 0
248
+ # date.
249
+ value = int(value * 10000000)
250
+
251
+ if isinstance(value, int):
252
+ value = constants.st.ST_LE_UI64.pack(value)
253
+
254
+ if not isinstance(value, bytes):
255
+ raise ValueError(f'Failed to convert value to bytes (expected bytes at end, got {type(value)}). Please report to developer.')
256
+
257
+ return constants.st.ST_PROP_BASE.pack(self.type, self.propertyID, self.flags) + value
258
+
259
+ @property
260
+ def signedValue(self) -> Any:
261
+ """
262
+ A signed representation of the value.
263
+
264
+ Setting the value through this property will convert it if necessary
265
+ before using the default value setter.
266
+
267
+ :raises struct.error: The value was out of range when setting.
268
+ """
269
+ if self.type == 0x0002:
270
+ return constants.st.ST_SBO_I16.unpack(constants.st.ST_SBO_UI16.pack(self.value))[0]
271
+ if self.type == 0x0003:
272
+ return constants.st.ST_SBO_I32.unpack(constants.st.ST_SBO_UI32.pack(self.value))[0]
273
+ if self.type == 0x0014:
274
+ return constants.st.ST_SBO_I64.unpack(constants.st.ST_SBO_UI64.pack(self.value))[0]
275
+
276
+ # If not any of those types, return without modification.
277
+ return self.value
278
+
279
+ @signedValue.setter
280
+ def signedValue(self, value: Any) -> None:
281
+ if self.type == 0x0002:
282
+ value = constants.st.ST_SBO_UI16.unpack(constants.st.ST_SBO_I16.pack(value))[0]
283
+ if self.type == 0x0003:
284
+ value = constants.st.ST_SBO_UI32.unpack(constants.st.ST_SBO_I32.pack(value))[0]
285
+ if self.type == 0x0014:
286
+ value = constants.st.ST_SBO_UI64.unpack(constants.st.ST_SBO_I64.pack(value))[0]
287
+
288
+ self.value = value
289
+
290
+ @property
291
+ def value(self) -> Any:
292
+ """
293
+ Property value.
294
+ """
295
+ return self.__value
296
+
297
+ @value.setter
298
+ def value(self, value: Any) -> None:
299
+ # Validate the value and perform necessary conversions.
300
+ if self.type == 0x0000: # Unspecified.
301
+ if not isinstance(value, bytes):
302
+ raise TypeError(':property value: MUST be bytes when type is 0x0000.')
303
+ if len(value) != 8:
304
+ raise ValueError(':property value: MUST be 8 bytes when type is 0x0000.')
305
+ elif self.type == 0x0001: # Null.
306
+ raise TypeError(':property value: cannot be set when type is 0x0001.')
307
+ elif self.type in (0x0002, 0x0003, 0x0014): # Ints.
308
+ if not isinstance(value, int):
309
+ raise TypeError(f':property value: MUST be an int when type is 0x{self.type:04X}')
310
+ if value < 0:
311
+ raise ValueError(f':property value: MUST be positive when type is 0x{self.type:04X}.')
312
+ if self.type == 0x0002:
313
+ if value > 0xFFFF:
314
+ raise ValueError(':property value: MUST be less than 0x10000 when type is 0x0002.')
315
+ elif self.type == 0x0003:
316
+ if value > 0xFFFFFFFF:
317
+ raise ValueError(':property value: MUST be less than 0x100000000 when type is 0x0003.')
318
+ elif self.type == 0x0014:
319
+ if value > 0xFFFFFFFFFFFFFFFF:
320
+ raise ValueError(':property value: MUST be less than 0x10000000000000000 when type is 0x0014.')
321
+ elif self.type == 0x0004 or self.type == 0x0005: # Float/Double.
322
+ if not isinstance(value, float):
323
+ try:
324
+ value = float(value)
325
+ except Exception:
326
+ raise TypeError(f':property value: MUST be float or convertable to float when type is 0x{self.type:04X}.')
327
+ elif self.type == 0x0006:
328
+ if not isinstance(value, decimal.Decimal):
329
+ try:
330
+ value = decimal.Decimal(value)
331
+ except decimal.InvalidOperation:
332
+ raise ValueError(':property value: MUST be Decimal or convertable to Decimal when type is 0x0006.')
333
+ except TypeError:
334
+ raise TypeError(':property value: MUST be Decimal or convertable to Decimal when type is 0x0006.')
335
+ if (value * 10000) > 0xFFFFFFFFFFFFFFFF:
336
+ raise ValueError('Decimal value is too large (must be less than 0x10000000000000000 when multiplied by 10000).')
337
+ elif self.type == 0x0007 or self.type == 0x0040:
338
+ if not isinstance(value, datetime.datetime):
339
+ raise TypeError(f':property value: MUST be an instance of datetime when type is 0x{self.type:04X}.')
340
+ elif self.type == 0x000A:
341
+ if not isinstance(value, int):
342
+ raise TypeError(':property value: MUST be an int or instance of ErrorCodeType when type is 0x000A.')
343
+ if not isinstance(value, ErrorCodeType):
344
+ if value > 0xFFFFFFFF:
345
+ raise ValueError(':property value: MUST be less than 0x100000000 when type is 0x000A and the value is not an instance of ErrorCodeType.')
346
+ # Convert only if possible.
347
+ try:
348
+ value = ErrorCodeType(value)
349
+ except ValueError:
350
+ # Ignore it if the conversion isn't possible.
351
+ pass
352
+ elif self.type == 0x000B:
353
+ if not isinstance(value, bool):
354
+ raise TypeError(f':property value: MUST be bool when type is 0x{self.type:04X}.')
355
+
356
+ self.__value = value
357
+
358
+
359
+
360
+ class VariableLengthProp(PropBase):
361
+ """
362
+ Class to contain the data for a single variable length property.
363
+ """
364
+
365
+ def __init__(self, data: bytes):
366
+ super().__init__(data)
367
+ self.__length, self.__reserved = constants.st.ST_PROP_VAR.unpack(data[8:])
368
+ if self.type == 0x001E:
369
+ # Check that the value is valid.
370
+ if self.__length == 0:
371
+ logger.warning('Property of type 0x001E found with length that was not at least 1. This will be corrected automatically.')
372
+ self.__length = 1
373
+ self.__realLength = self.__length - 1
374
+ elif self.type == 0x001F:
375
+ if self.__length == 0:
376
+ logger.warning('Property of type 0x001F found with length that was not at least 2. This will be corrected automatically.')
377
+ self.__length = 2
378
+ if self.__length & 1:
379
+ logger.warning('Property of type 0x001F found with length that is not a multiple of 2. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
380
+ self.__realLength = self.__length // 2 - 1
381
+ elif self.type in constants.MULTIPLE_2_BYTES_HEX:
382
+ if self.__length & 1:
383
+ logger.warning(f'Property of type {self.type} found with length that is not a multiple of 2. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
384
+ self.__realLength = self.__length // 2
385
+ elif self.type in constants.MULTIPLE_4_BYTES_HEX:
386
+ if self.__length & 3:
387
+ logger.warning(f'Property of type {self.type} found with length that is not a multiple of 4. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
388
+ self.__realLength = self.__length // 4
389
+ elif self.type in constants.MULTIPLE_8_BYTES_HEX:
390
+ if self.__length & 7:
391
+ logger.warning(f'Property of type {self.type} found with length that is not a multiple of 8. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
392
+ self.__realLength = self.__length // 8
393
+ elif self.type in constants.MULTIPLE_16_BYTES_HEX:
394
+ if self.__length & 15:
395
+ logger.warning(f'Property of type {self.type} found with length that is not a multiple of 16. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
396
+ self.__realLength = self.__length // 16
397
+ elif self.type == 0x000D:
398
+ self.__realLength = 0xFFFFFFFF
399
+ # Check the value and log a warning if it is bad before fixing it.
400
+ if self.__length != 0xFFFFFFFF:
401
+ logger.warning(f'Property of type 0x000D found with length that was not 0xFFFFFFFF (got {self.__length:08X}). This will be corrected automatically.')
402
+ self.__length = 0xFFFFFFFF
403
+ elif self.type == 0x0048:
404
+ self.__realLength = 16
405
+ # Check the value and log a warning if it is bad before fixing it.
406
+ if self.__length != 16:
407
+ logger.warning(f'Property of type 0x0048 found with length that was not 16 (got {self.__length}). This will be corrected automatically.')
408
+ self.__length = 16
409
+ elif self.type == 0x101E or self.type == 0x101F:
410
+ if self.__length & 3:
411
+ logger.warning(f'Property of type {self.type} found with length that is not a multiple of 4. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
412
+ self.__realLength = self.__length // 4
413
+ elif self.type == 0x1102:
414
+ if self.__length & 7:
415
+ logger.warning(f'Property of type {self.type} found with length that is not a multiple of 8. This will not be corrected but is likely an error. This may cause issues with reading this property in other programs.')
416
+ self.__realLength = self.__length // 8
417
+ else:
418
+ if self.type in (0x00FB, 0x00FF, 0x00FE):
419
+ # None of these are properly implemented, but we don't want to
420
+ # actually raise an exception because of them. So just log the
421
+ # issue.
422
+ logger.error(f'Property type {self.type} has no documentation in [MS-OXMSG] but was found on this file. Please report this to the developer.')
423
+ self.__realLength = self.__length
424
+
425
+ def toBytes(self) -> bytes:
426
+ ret = constants.st.ST_PROP_BASE.pack(self.type, self.propertyID, self.flags)
427
+ ret += constants.st.ST_PROP_VAR.pack(self.__length, self.__reserved)
428
+ return ret
429
+
430
+ @property
431
+ def size(self) -> int:
432
+ """
433
+ The size of the data the property corresponds to.
434
+
435
+ For string streams, this is the number of characters contained. For
436
+ multiple properties, this is the number of entries.
437
+
438
+ When setting this, the underlying length field will be set which is a
439
+ manipulated value. For multiple properties of a fixed length, this will
440
+ be the size value multiplied by the length of the properties. For
441
+ multiple strings, this will be 4 times the size value. For multiple
442
+ binary, this will be 8 times the size value. For strings, this will be
443
+ the number of characters plus 1 if non-unicode, otherwise the number of
444
+ characters plus 1, all multiplied by 2. For binary, this will be the
445
+ size with no modification.
446
+
447
+ Size cannot be set for properties of type 0x000D and 0x0048.
448
+
449
+ :raises TypeError: Tried to set the size for a property that cannot have
450
+ the size changed.
451
+ :raises ValueError: The translated value for the size is too large when
452
+ setting.
453
+ """
454
+ return self.__realLength
455
+
456
+ @size.setter
457
+ def size(self, value: int) -> None:
458
+ if not isinstance(value, int):
459
+ raise TypeError(':property size: MUST be an int.')
460
+ if value < 0:
461
+ raise ValueError(':property size: MUST be positive.')
462
+ if self.type == 0x000D or self.type == 0x0048:
463
+ raise TypeError(f':property size: cannot be set for 0x{self.type:04X}.')
464
+
465
+ # Convert the size to the actual length value.
466
+ if self.type == 0x001E:
467
+ length = value + 1
468
+ elif self.type == 0x001F:
469
+ length = (value + 1) * 2
470
+ elif self.type in constants.MULTIPLE_2_BYTES_HEX:
471
+ length = value * 2
472
+ elif self.type in constants.MULTIPLE_4_BYTES_HEX:
473
+ length = value * 4
474
+ elif self.type in constants.MULTIPLE_8_BYTES_HEX:
475
+ length = value * 8
476
+ elif self.type in constants.MULTIPLE_16_BYTES_HEX:
477
+ length = value * 16
478
+ elif self.type == 0x101E or self.type == 0x101F:
479
+ length = value * 4
480
+ elif self.type == 0x1102:
481
+ length = value * 8
482
+
483
+ # Validate the range of the length value.
484
+ if length > 0xFFFFFFFF:
485
+ raise ValueError(f'Calculated length value is too large (max is 0xFFFFFFFF, got {length:08X}).')
486
+
487
+ self.__length = length
488
+ self.__realLength = value
489
+
490
+ @property
491
+ def reservedFlags(self) -> int:
492
+ """
493
+ The reserved flags field of the variable length property.
494
+ """
495
+ return self.__reserved
496
+
497
+ @reservedFlags.setter
498
+ def reservedFlags(self, value: int) -> None:
499
+ if not isinstance(value, int):
500
+ raise TypeError(':property reservedFlags: MUST be an int.')
501
+ if value < 0:
502
+ raise ValueError(':property reservedFlags: MUST be positive.')
503
+ if value > 0xFFFFFFFF:
504
+ raise ValueError(':property reservedFlags: MUST be less than 0x100000000.')
505
+
506
+ self.__reserved = value