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,1172 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'MSGFile',
6
+ ]
7
+
8
+
9
+ import codecs
10
+ import copy
11
+ import datetime
12
+ import functools
13
+ import io
14
+ import logging
15
+ import os
16
+ import pathlib
17
+ import weakref
18
+ import zipfile
19
+
20
+ import olefile
21
+
22
+ from typing import (
23
+ Any, Callable, cast, Dict, List, Optional, Tuple, TypeVar, Union
24
+ )
25
+
26
+ from .. import constants
27
+ from ..constants import (
28
+ DATE_FORMAT, DT_FORMAT, MSG_PATH, OVERRIDE_CLASS, ps, SAVE_TYPE
29
+ )
30
+ from ..attachments import (
31
+ AttachmentBase, initStandardAttachment, SignedAttachment
32
+ )
33
+ from ..encoding import lookupCodePage
34
+ from ..enums import (
35
+ ErrorBehavior, InsecureFeatures, Importance, Priority, PropertiesType,
36
+ RetentionFlags, SaveType, Sensitivity, SideEffect
37
+ )
38
+ from ..exceptions import (
39
+ ConversionError, InvalidFileFormatError, PrefixError,
40
+ StandardViolationError
41
+ )
42
+ from ..properties.named import Named, NamedProperties
43
+ from ..properties.properties_store import PropertiesStore
44
+ from ..structures.contact_link_entry import ContactLinkEntry
45
+ from ..utils import (
46
+ divide, guessEncoding, inputToMsgPath, makeWeakRef, msgPathToString,
47
+ parseType, verifyPropertyId, verifyType
48
+ )
49
+
50
+
51
+ logger = logging.getLogger(__name__)
52
+ logger.addHandler(logging.NullHandler())
53
+
54
+ _T = TypeVar('_T')
55
+
56
+
57
+ class MSGFile:
58
+ """
59
+ Base handler for all .msg files.
60
+ """
61
+
62
+ filename: Optional[str]
63
+
64
+ def __init__(self, path, **kwargs):
65
+ """
66
+ :param path: Path to the MSG file in the system or the bytes of the MSG
67
+ file.
68
+ :param prefix: Used for extracting embedded MSG files inside the main
69
+ one. Do not set manually unless you know what you are doing.
70
+ :param parentMsg: Used for synchronizing instances of shared objects. Do
71
+ not set this unless you know what you are doing.
72
+ :param initAttachment: Optional, the method used when creating an
73
+ attachment for an MSG file. MUST be a function that takes 2
74
+ arguments (the ``MSGFile`` instance and the directory in the MSG
75
+ file where the attachment is) and returns an instance of
76
+ ``AttachmentBase``.
77
+ :param delayAttachments: Optional, delays the initialization of
78
+ attachments until the user attempts to retrieve them. Allows MSG
79
+ files with bad attachments to be initialized so the other data can
80
+ be retrieved.
81
+ :param filename: Optional, the filename to be used by default when
82
+ saving.
83
+ :param errorBehavior: Optional, the behavior to use in the event of
84
+ certain types of errors. Uses the ``ErrorBehavior`` enum.
85
+ :param overrideEncoding: Optional, an encoding to use instead of the one
86
+ specified by the MSG file. If the value is ``"chardet"`` and you
87
+ have the ``chardet`` module installed, an attempt will be made to
88
+ auto-detect the encoding based on some of the string properties. Do
89
+ not report encoding errors caused by this.
90
+ :param treePath: Internal variable used for giving representation of the
91
+ path, as a tuple of objects, of the ``MSGFile``. When passing, this
92
+ is the path to the parent object of this instance.
93
+ :param insecureFeatures: Optional, an enum value that specifies if
94
+ certain insecure features should be enabled. These features should
95
+ only be used on data that you trust. Uses the ``InsecureFeatures``
96
+ enum.
97
+ :param dateFormat: Optional, the format string to use for dates.
98
+ :param datetimeFormat: Optional, the format string to use for dates
99
+ that include a time component.
100
+
101
+ :raises InvalidFileFormatError: The file is not an OLE file or could
102
+ not be parsed as an MSG file.
103
+ :raises StandardViolationError: Some part of the file badly violates the
104
+ standard.
105
+ :raises IOError: There is an issue opening the MSG file.
106
+ :raises NameError: The encoding provided is not supported.
107
+ :raises PrefixError: The prefix is not a supported type.
108
+ :raises TypeError: The parent is not an instance of ``MSGFile`` or a
109
+ subclass.
110
+ :raises ValueError: The path is invalid.
111
+
112
+ It's recommended to check the error message to ensure you know why a
113
+ specific exception was raised.
114
+ """
115
+ # Retrieve all the kwargs that we need.
116
+ self.__inscFeat: InsecureFeatures = kwargs.get('insecureFeatures', InsecureFeatures.NONE)
117
+ prefix: str = cast(str, kwargs.get('prefix', ''))
118
+ self.__parentMsg = makeWeakRef(cast(MSGFile, kwargs.get('parentMsg')))
119
+ self.__treePath = kwargs.get('treePath', []) + [weakref.ref(self)]
120
+ # Verify it is a valid class.
121
+ if self.__parentMsg and not isinstance(self.__parentMsg(), MSGFile):
122
+ raise TypeError(':param parentMsg: must be an instance of MSGFile or a subclass.')
123
+ filename = kwargs.get('filename')
124
+ overrideEncoding = kwargs.get('overrideEncoding')
125
+
126
+ # WARNING DO NOT MANUALLY MODIFY PREFIX. Let the program set it.
127
+ self.__path = path
128
+ self.__initAttachmentFunc = kwargs.get('initAttachment', initStandardAttachment)
129
+ self.__attachmentsDelayed = bool(kwargs.get('delayAttachments', False))
130
+ self.__attachmentsReady = False
131
+ self.__errorBehavior = ErrorBehavior(kwargs.get('errorBehavior', ErrorBehavior.THROW))
132
+ self.__dateFormat = kwargs.get('dateFormat', DATE_FORMAT)
133
+ self.__dtFormat = kwargs.get('datetimeFormat', DT_FORMAT)
134
+
135
+ self.__listDirRes: Dict[Tuple[bool, bool, bool], List[List[str]]] = {}
136
+
137
+ if self.__parentMsg:
138
+ # We should be able to directly access the private variables of
139
+ # another instance with no issue.
140
+ if (msg := self.__parentMsg()) is not None:
141
+ self.__ole = msg.__ole
142
+ self.__oleOwner = False
143
+ else:
144
+ raise ReferenceError('Parent MSG was garbage collected during init of child msg.')
145
+ else:
146
+ # Verify the path at least evaluates to True, as not doing so can
147
+ # allow an OleFile to be created without a path.
148
+ if not path:
149
+ raise ValueError(':param path: must be set and must not be empty.')
150
+ try:
151
+ if ErrorBehavior.OLE_DEFECT_INCORRECT in self.errorBehavior:
152
+ defect = olefile.DEFECT_FATAL
153
+ else:
154
+ defect = olefile.DEFECT_INCORRECT
155
+ self.__ole = olefile.OleFileIO(path, raise_defects = defect)
156
+ except OSError as e:
157
+ logger.error(e)
158
+ if str(e) == 'not an OLE2 structured storage file':
159
+ raise InvalidFileFormatError(e)
160
+ else:
161
+ raise
162
+ # This is a variable that tells whether we own the olefile. Used for
163
+ # closing. We set it here for error handling.
164
+ self.__oleOwner = True
165
+
166
+ self.__open = True
167
+
168
+ # The rest *must* be in a try-except block to ensure we close the file.
169
+ try:
170
+ kwargsCopy = copy.copy(kwargs)
171
+ if 'prefix' in kwargsCopy:
172
+ del kwargsCopy['prefix']
173
+ if 'parentMsg' in kwargsCopy:
174
+ del kwargsCopy['parentMsg']
175
+ if 'filename' in kwargsCopy:
176
+ del kwargsCopy['filename']
177
+ if 'treePath' in kwargsCopy:
178
+ del kwargsCopy['treePath']
179
+ self.__kwargs = kwargsCopy
180
+
181
+ prefixl = []
182
+ if prefix:
183
+ try:
184
+ prefixl = inputToMsgPath(prefix)
185
+ prefix = '/'.join(prefixl) + '/'
186
+ except ConversionError:
187
+ raise PrefixError(f'The provided prefix could not be used: {prefix}')
188
+ self.__prefix = prefix
189
+ self.__prefixList = prefixl
190
+ self.__prefixLen = len(prefixl)
191
+
192
+ if overrideEncoding is not None:
193
+ logger.warning('You have chosen to override the string encoding. Do not report encoding errors caused by this.')
194
+ if overrideEncoding.lower() == 'chardet':
195
+ encoding = guessEncoding(self)
196
+ if encoding:
197
+ self.__stringEncoding = encoding.lower()
198
+ else:
199
+ logger.warning('Attempted to auto-detect encoding, but no consensus could be formed based on the top-level strings. Defaulting to normal detection methods.')
200
+ else:
201
+ codecs.lookup(overrideEncoding)
202
+ self.__stringEncoding = overrideEncoding
203
+ self.__overrideEncoding = overrideEncoding
204
+
205
+ if prefix and not filename:
206
+ filename = self.getStringStream(prefixl[:-1] + ['__substg1.0_3001'], prefix = False)
207
+ if filename:
208
+ self.filename = filename
209
+ elif hasattr(path, '__len__'):
210
+ if len(path) < 1536:
211
+ self.filename = str(path)
212
+ else:
213
+ self.filename = None
214
+ elif isinstance(path, pathlib.Path):
215
+ self.filename = str(path)
216
+ else:
217
+ self.filename = None
218
+
219
+ # Now, load the attachments if we are not delaying them.
220
+ if not self.__attachmentsDelayed:
221
+ self.attachments
222
+ except:
223
+ # *Any* exception here requires that we close the file.
224
+ try:
225
+ self.close()
226
+ except:
227
+ pass
228
+ # Raise the exception after trying to close the file.
229
+ raise
230
+
231
+ def __bytes__(self) -> bytes:
232
+ return self.exportBytes()
233
+
234
+ def __enter__(self) -> MSGFile:
235
+ self.__ole.__enter__()
236
+ return self
237
+
238
+ def __exit__(self, *_) -> None:
239
+ self.close()
240
+
241
+ def _getOleEntry(self, filename: MSG_PATH, prefix: bool = True) -> olefile.olefile.OleDirectoryEntry:
242
+ """
243
+ Finds the directory entry from the OLE file for the stream or storage
244
+ specified.
245
+
246
+ Use ``'/'`` to get the root entry.
247
+
248
+ :param prefix: Bool, whether to search for the entry at the root of the
249
+ MSG file (``False``) or look in the current child MSG file
250
+ (``True``). (Default: ``True``)
251
+ """
252
+ sid = -1
253
+ if filename == '/':
254
+ if prefix and self.__prefix:
255
+ sid = self.__ole._find(self.__prefixList)
256
+ else:
257
+ return self.__ole.direntries[0]
258
+ else:
259
+ sid = self.__ole._find(self.fixPath(filename, prefix))
260
+
261
+ return self.__ole.direntries[sid]
262
+
263
+ def _getTypedAs(self, _id: str, overrideClass = None, preserveNone: bool = True):
264
+ """
265
+ Like the other "get as" functions, but designed for when something
266
+ could be multiple types (where only one will be present).
267
+
268
+ This way you have no need to set the type, it will be handled for you.
269
+
270
+ :param overrideClass: Class/function to use to morph the data that was
271
+ read. The data will be the first argument to the class's
272
+ ``__init__`` method or the function itself, if that is what is
273
+ provided. By default, this will be completely ignored if the value was not found.
274
+ :param preserveNone: If ``True`` (default), causes the function to
275
+ ignore :param overrideClass: when the value could not be found (is
276
+ ``None``). If this is changed to ``False``, then the value will be
277
+ used regardless.
278
+ """
279
+ value = self._getTypedData(_id)
280
+ # Check if we should be overriding the data type for this instance.
281
+ if overrideClass is not None:
282
+ if value is not None or not preserveNone:
283
+ value = overrideClass(value)
284
+
285
+ return value
286
+
287
+ def _getTypedData(self, _id: str, _type = None, prefix: bool = True):
288
+ """
289
+ Gets the data for the specified id as the type that it is supposed to
290
+ be.
291
+
292
+ :param _id: MUST be a 4 digit hexadecimal string.
293
+ :param prefix: Bool, whether to search for the entry at the root of the
294
+ MSG file (``False``) or look in the current child MSG file
295
+ (``True``). (Default: ``True``)
296
+
297
+ If you know for sure what type the data is before hand, you can specify
298
+ it as being one of the strings in the constant
299
+ ``FIXED_LENGTH_PROPS_STRING`` or ``VARIABLE_LENGTH_PROPS_STRING``.
300
+ """
301
+ verifyPropertyId(_id)
302
+ _id = _id.upper()
303
+ found, result = self._getTypedStream('__substg1.0_' + _id, prefix, _type)
304
+ if found:
305
+ return result
306
+ else:
307
+ found, result = self._getTypedProperty(_id, _type)
308
+ return result if found else None
309
+
310
+ def _getTypedProperty(self, propertyID: str, _type = None) -> Tuple[bool, Optional[Any]]:
311
+ """
312
+ Gets the property with the specified id as the type that it is supposed
313
+ to be.
314
+
315
+ :param propertyID: MUST be a 4 digit hexadecimal string.
316
+
317
+ If you know for sure what type the property is before hand, you can
318
+ specify it as being one of the strings in the constant
319
+ ``FIXED_LENGTH_PROPS_STRING`` or ``VARIABLE_LENGTH_PROPS_STRING``.
320
+ """
321
+ verifyPropertyId(propertyID)
322
+ if _type:
323
+ verifyType(_type)
324
+ propertyID += _type
325
+
326
+ notFound = object()
327
+ ret = self.getPropertyVal(propertyID, notFound)
328
+ if ret is notFound:
329
+ return False, None
330
+
331
+ return True, ret
332
+
333
+ def _getTypedStream(self, filename: MSG_PATH, prefix: bool = True, _type: Optional[str] = None) -> Tuple[bool, Optional[Any]]:
334
+ """
335
+ Gets the contents of the specified stream as the type that it is
336
+ supposed to be.
337
+
338
+ Rather than the full filename, you should only feed this function the
339
+ filename sans the type. So if the full name is "__substg1.0_001A001F",
340
+ the filename this function should receive should be "__substg1.0_001A".
341
+
342
+ If you know for sure what type the stream is before hand, you can
343
+ specify it as being one of the strings in the constant
344
+ ``FIXED_LENGTH_PROPS_STRING`` or ``VARIABLE_LENGTH_PROPS_STRING``.
345
+
346
+ If you have not specified the type, the type this function returns in
347
+ many cases cannot be predicted. As such, when using this function it is
348
+ best for you to check the type that it returns. If the function returns
349
+ None, that means it could not find the stream specified.
350
+
351
+ :param prefix: Bool, whether to search for the entry at the root of the
352
+ MSG file (``False``) or look in the current child MSG file
353
+ (``True``). (Default: ``True``)
354
+ """
355
+ verifyType(_type)
356
+ filename = self.fixPath(filename, prefix)
357
+ for x in (filename + _type,) if _type is not None else self.slistDir():
358
+ if x.startswith(filename) and '-' not in x:
359
+ if (contents := self.getStream(x, False)) is None:
360
+ continue
361
+ if len(contents) == 0:
362
+ return True, None # We found the file, but it was empty.
363
+ extras: List[bytes]= []
364
+ _type = x[-4:]
365
+ if x[-4] == '1': # It's a multiple
366
+ if _type in ('101F', '101E'):
367
+ streams = len(contents) // 4 # These lengths are normal.
368
+ elif _type == '1102':
369
+ streams = len(contents) // 8 # These lengths have 4 0x00 bytes at the end for seemingly no reason. They are "reserved" bytes
370
+ elif _type in ('1002', '1003', '1004', '1005', '1007', '1014', '1040', '1048'):
371
+ try:
372
+ streams = self.props[x[-8:]].realLength
373
+ except (KeyError, AttributeError):
374
+ logger.error(f'Could not find matching VariableLengthProp for stream {x}')
375
+ streams = len(contents) // (2 if _type in constants.MULTIPLE_2_BYTES else 4 if _type in constants.MULTIPLE_4_BYTES else 8 if _type in constants.MULTIPLE_8_BYTES else 16)
376
+ else:
377
+ raise NotImplementedError(f'The stream specified is of type {_type}. We don\'t currently understand exactly how this type works. If it is mandatory that you have the contents of this stream, please create an issue labeled "NotImplementedError: _getTypedStream {_type}".')
378
+ if _type in ('101F', '101E', '1102'):
379
+ if self.exists(x + '-00000000', False):
380
+ for y in range(streams):
381
+ if self.exists((name := f'{x}-{y:08X}'), False):
382
+ extras.append(self.getStream(name, False))
383
+ elif _type in ('1002', '1003', '1004', '1005', '1007', '1014', '1040', '1048'):
384
+ extras = divide(contents, (2 if _type in constants.MULTIPLE_2_BYTES else 4 if _type in constants.MULTIPLE_4_BYTES else 8 if _type in constants.MULTIPLE_8_BYTES else 16))
385
+ contents = streams
386
+ return True, parseType(int(_type, 16), contents, self.stringEncoding, extras)
387
+ return False, None # We didn't find the stream.
388
+
389
+ def _oleListDir(self, streams: bool = True, storages: bool = False) -> List[List[str]]:
390
+ """
391
+ Calls :meth:`OleFileIO.listdir` from the OleFileIO instance associated
392
+ with this MSG file. Useful for if you need access to all the top level
393
+ streams if this is an embedded MSG file.
394
+
395
+ :returns: A list of the streams and or storages depending on the
396
+ arguments given.
397
+ """
398
+ return self.__ole.listdir(streams, storages)
399
+
400
+ def close(self) -> None:
401
+ if self.__open:
402
+ if self.attachmentsReady:
403
+ for attachment in self.attachments:
404
+ if attachment.type == 'msg':
405
+ attachment.data.close()
406
+
407
+ if self.__oleOwner:
408
+ self.__ole.close()
409
+
410
+ self.__open = False
411
+
412
+ def debug(self) -> None:
413
+ for dir_ in self.listDir():
414
+ if dir_[-1].endswith('001E') or dir_[-1].endswith('001F'):
415
+ print('Directory: ' + str(dir_[:-1]))
416
+ print(f'Contents: {self.getStream(dir_)}')
417
+
418
+ def exists(self, filename: MSG_PATH, prefix: bool = True) -> bool:
419
+ """
420
+ Checks if the stream exists in the MSG file.
421
+
422
+ :param prefix: Bool, whether to search for the entry at the root of the
423
+ MSG file (``False``) or look in the current child MSG file
424
+ (``True``). (Default: ``True``)
425
+ """
426
+ filename = self.fixPath(filename, prefix)
427
+ return self.__ole.exists(filename)
428
+
429
+ def sExists(self, filename: MSG_PATH, prefix: bool = True) -> bool:
430
+ """
431
+ Checks if string stream exists in the MSG file.
432
+
433
+ :param prefix: Bool, whether to search for the entry at the root of the
434
+ MSG file (``False``) or look in the current child MSG file
435
+ (``True``). (Default: ``True``)
436
+ """
437
+ filename = self.fixPath(filename, prefix)
438
+ return self.exists(filename + '001F') or self.exists(filename + '001E')
439
+
440
+ def existsTypedProperty(self, _id: str, location = None, _type = None, prefix: bool = True, propertiesInstance: Optional[PropertiesStore] = None) -> Tuple[bool, int]:
441
+ """
442
+ Determines if the stream with the provided id exists in the location
443
+ specified.
444
+
445
+ If no location is specified, the root directory is searched. The return of this function is 2 values, the first being a boolean for if anything
446
+ was found, and the second being how many were found.
447
+
448
+ Because of how this method works, any folder that contains it's own
449
+ "__properties_version1.0" file should have this method called from
450
+ it's class.
451
+
452
+ :param prefix: Bool, whether to search for the entry at the root of the
453
+ MSG file (``False``) or look in the current child MSG file
454
+ (``True``). (Default: ``True``)
455
+ """
456
+ verifyPropertyId(_id)
457
+ verifyType(_type)
458
+ _id = _id.upper()
459
+ if propertiesInstance is None:
460
+ propertiesInstance = self.props
461
+ prefixList = self.prefixList if prefix else []
462
+ if location is not None:
463
+ prefixList.append(location)
464
+ prefixList = inputToMsgPath(prefixList)
465
+ usableId = _id + _type if _type else _id
466
+ foundNumber = 0
467
+ foundStreams = []
468
+ for item in self.listDir():
469
+ if len(item) > self.__prefixLen:
470
+ if item[self.__prefixLen].startswith('__substg1.0_' + usableId) and item[self.__prefixLen] not in foundStreams:
471
+ foundNumber += 1
472
+ foundStreams.append(item[self.__prefixLen])
473
+ for x in propertiesInstance:
474
+ if x.startswith(usableId):
475
+ for y in foundStreams:
476
+ if y.endswith(x):
477
+ break
478
+ else:
479
+ foundNumber += 1
480
+ return (foundNumber > 0), foundNumber
481
+
482
+ def export(self, path) -> None:
483
+ """
484
+ Exports the contents of this MSG file to a new MSG files specified by
485
+ the path given.
486
+
487
+ If this is an embedded MSG file, the embedded streams and directories will be added to it as if they were at the root, allowing you to save it as it's own MSG file.
488
+
489
+ This function pulls directly from the source MSG file, so modifications
490
+ to the properties of of an ``MSGFile`` object (or one of it's
491
+ subclasses) will not be reflected in the saved file.
492
+
493
+ :param path: A path-like object (including strings and ``pathlib.Path``
494
+ objects) or an IO device with a write method which accepts bytes.
495
+ """
496
+ from ..ole_writer import OleWriter
497
+
498
+ # Create an instance of the class used for writing a new OLE file.
499
+ writer = OleWriter()
500
+ # Add all file and directory entries to it. If this
501
+ writer.fromMsg(self)
502
+ writer.write(path)
503
+
504
+ def exportBytes(self) -> bytes:
505
+ """
506
+ Saves a new copy of the MSG file, returning the bytes.
507
+ """
508
+ out = io.BytesIO()
509
+ self.export(out)
510
+ return out.getvalue()
511
+
512
+ def fixPath(self, inp: MSG_PATH, prefix: bool = True) -> str:
513
+ """
514
+ Changes paths so that they have the proper prefix (should :param prefix:
515
+ be ``True``) and are strings rather than lists or tuples.
516
+ """
517
+ inp = msgPathToString(inp)
518
+ if prefix:
519
+ inp = self.__prefix + inp
520
+ return inp
521
+
522
+ def getMultipleBinary(self, filename: MSG_PATH, prefix: bool = True) -> Optional[List[bytes]]:
523
+ """
524
+ Gets a multiple binary property as a list of ``bytes`` objects.
525
+
526
+ Like :meth:`getStringStream`, the 4 character type suffix should be
527
+ omitted. So if you want the stream "__substg1.0_00011102" then the
528
+ filename would simply be "__substg1.0_0001".
529
+
530
+ :param prefix: Bool, whether to search for the entry at the root of the
531
+ MSG file (``False``) or look in the current child MSG file
532
+ (``True``). (Default: ``True``)
533
+ """
534
+ filename = self.fixPath(filename, prefix) + '1102'
535
+ multStream = self.getStream(filename)
536
+ if multStream is None:
537
+ return None
538
+
539
+ if len(multStream) == 0:
540
+ return []
541
+ elif len(multStream) & 7 != 0:
542
+ raise StandardViolationError(f'Length stream for multiple binary was not a multiple of 8.')
543
+ else:
544
+ ret = [self.getStream(filename + f'-{x:08X}') for x in range(len(multStream) // 8)]
545
+ # We could do more checking here, but we'll just check for None.
546
+ if (index := next((x for x in ret if x is None), -1)) != -1:
547
+ logger.error('Unable to get the desired number of binary streams for multiple, not all streams were found.')
548
+ return ret[:index]
549
+ return ret
550
+
551
+ def getMultipleString(self, filename: MSG_PATH, prefix: bool = True) -> Optional[List[str]]:
552
+ """
553
+ Gets a multiple string property as a list of ``str`` objects.
554
+
555
+ Like :meth:`getStringStream`, the 4 character type suffix should be
556
+ omitted. So if you want the stream "__substg1.0_00011102" then the
557
+ filename would simply be "__substg1.0_0001".
558
+
559
+ :param prefix: Bool, whether to search for the entry at the root of the
560
+ MSG file (``False``) or look in the current child MSG file
561
+ (``True``). (Default: ``True``)
562
+ """
563
+ filename = self.fixPath(filename, prefix) + '101F' if self.areStringsUnicode else '101E'
564
+ multStream = self.getStream(filename)
565
+ if multStream is None:
566
+ return []
567
+
568
+ if len(multStream) == 0:
569
+ return []
570
+ elif len(multStream) & 3 != 0:
571
+ raise StandardViolationError(f'Length stream for multiple string was not a multiple of 4.')
572
+ else:
573
+ ret = [self.getStream(filename + f'-{x:08X}') for x in range(len(multStream) // 4)]
574
+ # We could do more checking here, but we'll just check for None.
575
+ for index, item in enumerate(ret):
576
+ if item is None:
577
+ logger.error('Unable to get the desired number of string streams for multiple, not all streams were found.')
578
+ return ret[:index]
579
+ # Decode the bytes and remove the null byte.
580
+ ret[index] = item.decode(self.stringEncoding)[:-1]
581
+ return ret
582
+
583
+ def getNamedAs(self, propertyName: str, guid: str, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
584
+ """
585
+ Returns the named property, setting the class if specified.
586
+
587
+ :param overrideClass: Class/function to use to morph the data that was
588
+ read. The data will be the first argument to the class's
589
+ ``__init__`` method or the function itself, if that is what is
590
+ provided. If the value is ``None``, this function is not called. If
591
+ you want it to be called regardless, you should handle the data
592
+ directly.
593
+ """
594
+ value = self.getNamedProp(propertyName, guid)
595
+ if value is not None:
596
+ value = overrideClass(value)
597
+ return value
598
+
599
+ def getNamedProp(self, propertyName: str, guid: str, default: _T = None) -> Union[Any, _T]:
600
+ """
601
+ instance.namedProperties.get((propertyName, guid), default)
602
+
603
+ Can be override to create new behavior.
604
+ """
605
+ return self.namedProperties.get((propertyName, guid), default)
606
+
607
+ def getPropertyAs(self, propertyName: Union[int, str], overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
608
+ """
609
+ Returns the property, setting the class if found.
610
+
611
+ :param overrideClass: Class/function to use to morph the data that was
612
+ read. The data will be the first argument to the class's
613
+ ``__init__`` method or the function itself, if that is what is
614
+ provided. If the value is ``None``, this function is not called. If
615
+ you want it to be called regardless, you should handle the data
616
+ directly.
617
+ """
618
+ value = self.getPropertyVal(propertyName)
619
+
620
+ if value is not None:
621
+ value = overrideClass(value)
622
+
623
+ return value
624
+
625
+ def getPropertyVal(self, name: Union[int, str], default: _T = None) -> Union[Any, _T]:
626
+ """
627
+ instance.props.getValue(name, default)
628
+
629
+ Can be overridden to create new behavior.
630
+ """
631
+ return self.props.getValue(name, default)
632
+
633
+ def getSingleOrMultipleBinary(self, filename: MSG_PATH, prefix: bool = True) -> Optional[Union[List[bytes], bytes]]:
634
+ """
635
+ Combination of :meth:`getStream` and :meth:`getMultipleBinary`.
636
+
637
+ Checks to see if a single binary stream exists to return, otherwise
638
+ tries to return the multiple binary stream of the same ID.
639
+
640
+ Like :meth:`getStringStream`, the 4 character type suffix should be
641
+ omitted. So if you want the stream "__substg1.0_00010102" then the
642
+ filename would simply be "__substg1.0_0001".
643
+
644
+ :param prefix: Bool, whether to search for the entry at the root of the
645
+ MSG file (``False``) or look in the current child MSG file
646
+ (``True``). (Default: ``True``)
647
+ """
648
+ filename = self.fixPath(filename, prefix)
649
+ # Check for a single binary stream first.
650
+ if (ret := self.getStream(filename + '0102', False)) is not None:
651
+ return ret
652
+ # Otherwise, we just let the return from `getMultipleBinary` do the
653
+ # work.
654
+ return self.getMultipleBinary(filename, False)
655
+
656
+ def getSingleOrMultipleString(self, filename: MSG_PATH, prefix: bool = True) -> Optional[Union[List[str], str]]:
657
+ """
658
+ Combination of :meth:`getStringStream` and :meth:`getMultipleString`.
659
+
660
+ Checks to see if a single string stream exists to return, otherwise
661
+ tries to return the multiple string stream of the same ID.
662
+
663
+ Like :meth:`getStringStream`, the 4 character type suffix should be
664
+ omitted. So if you want the stream "__substg1.0_0001001F" then the
665
+ filename would simply be "__substg1.0_0001".
666
+
667
+ :param prefix: Bool, whether to search for the entry at the root of the
668
+ MSG file (``False``) or look in the current child MSG file
669
+ (``True``). (Default: ``True``)
670
+ """
671
+ filename = self.fixPath(filename, prefix)
672
+ # Check for a single stribng stream first.
673
+ if (ret := self.getStringStream(filename, False)) is not None:
674
+ return ret
675
+ # Otherwise, we just let the return from `getMultipleString` do the
676
+ # work.
677
+ return self.getMultipleString(filename, False)
678
+
679
+ def getStream(self, filename: MSG_PATH, prefix: bool = True) -> Optional[bytes]:
680
+ """
681
+ Gets a binary representation of the requested stream.
682
+
683
+ This should ALWAYS return a ``bytes`` object if it was found, otherwise
684
+ returns ``None``.
685
+
686
+ :param prefix: Bool, whether to search for the entry at the root of the
687
+ MSG file (``False``) or look in the current child MSG file
688
+ (``True``). (Default: ``True``)
689
+ """
690
+ filename = self.fixPath(filename, prefix)
691
+ if self.exists(filename, False):
692
+ with self.__ole.openstream(filename) as stream:
693
+ return stream.read() or b''
694
+ else:
695
+ logger.info(f'Stream "{filename}" was requested but could not be found. Returning `None`.')
696
+ return None
697
+
698
+ def getStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
699
+ """
700
+ Returns the specified stream, modifying it to the specified class if it
701
+ is found.
702
+
703
+ :param overrideClass: Class/function to use to morph the data that was
704
+ read. The data will be the first argument to the class's
705
+ ``__init__`` method or the function itself, if that is what is
706
+ provided. If the value is ``None``, this function is not called. If
707
+ you want it to be called regardless, you should handle the data
708
+ directly.
709
+ """
710
+ value = self.getStream(streamID)
711
+
712
+ if value is not None:
713
+ value = overrideClass(value)
714
+
715
+ return value
716
+
717
+ def getStringStream(self, filename: MSG_PATH, prefix: bool = True) -> Optional[str]:
718
+ """
719
+ Gets a string representation of the requested stream.
720
+
721
+ Rather than the full filename, you should only feed this function the
722
+ filename sans the type. So if the full name is "__substg1.0_001A001F",
723
+ the filename this function should receive should be "__substg1.0_001A".
724
+
725
+ This should ALWAYS return a string if it was found, otherwise returns
726
+ ``None``.
727
+
728
+ :param prefix: Bool, whether to search for the entry at the root of the
729
+ MSG file (``False``) or look in the current child MSG file
730
+ (``True``). (Default: ``True``)
731
+ """
732
+ filename = self.fixPath(filename, prefix)
733
+ if self.areStringsUnicode:
734
+ tmp = self.getStream(filename + '001F', prefix = False)
735
+ else:
736
+ tmp = self.getStream(filename + '001E', prefix = False)
737
+
738
+ return None if tmp is None else tmp.decode(self.stringEncoding)
739
+
740
+ def getStringStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
741
+ """
742
+ Returns the specified string stream, modifying it to the specified
743
+ class if it is found.
744
+
745
+ :param overrideClass: Class/function to use to morph the data that was
746
+ read. The data will be the first argument to the class's
747
+ ``__init__`` method or the function itself, if that is what is
748
+ provided. If the value is ``None``, this function is not called. If
749
+ you want it to be called regardless, you should handle the data
750
+ directly.
751
+ """
752
+ value = self.getStream(streamID)
753
+
754
+ if value is not None:
755
+ value = overrideClass(value)
756
+
757
+ return value
758
+
759
+ def listDir(self, streams: bool = True, storages: bool = False, includePrefix: bool = True) -> List[List[str]]:
760
+ """
761
+ Replacement for ``OleFileIO.listdir`` that runs at the current prefix
762
+ directory.
763
+
764
+ :param includePrefix: If ``False``, removes the part of the path that
765
+ is the prefix.
766
+ """
767
+ # Get the items from OleFileIO.
768
+ try:
769
+ return self.__listDirRes[(streams, storages, includePrefix)]
770
+ except KeyError:
771
+ entries = self.__ole.listdir(streams, storages)
772
+ if not self.__prefix:
773
+ return entries
774
+ prefix = self.__prefix.split('/')
775
+ if prefix[-1] == '':
776
+ prefix.pop()
777
+
778
+ prefixLength = self.__prefixLen
779
+ entries = [x for x in entries if len(x) > prefixLength and x[:prefixLength] == prefix]
780
+ if not includePrefix:
781
+ entries = [x[prefixLength:] for x in entries]
782
+ self.__listDirRes[(streams, storages, includePrefix)] = entries
783
+
784
+ return entries
785
+
786
+ def slistDir(self, streams: bool = True, storages: bool = False, includePrefix: bool = True) -> List[str]:
787
+ """
788
+ Replacement for OleFileIO.listdir that runs at the current prefix
789
+ directory. Returns a list of strings instead of lists.
790
+ """
791
+ return [msgPathToString(x) for x in self.listDir(streams, storages, includePrefix)]
792
+
793
+ def save(self, **kwargs) -> SAVE_TYPE:
794
+ if kwargs.get('skipNotImplemented', False):
795
+ return (SaveType.NONE, None)
796
+
797
+ raise NotImplementedError(f'Saving is not yet supported for the {self.__class__.__name__} class.')
798
+
799
+ def saveAttachments(self, skipHidden: bool = False, **kwargs) -> None:
800
+ """
801
+ Saves only attachments in the same folder.
802
+
803
+ :param skipHidden: If ``True``, skips attachments marked as hidden.
804
+ (Default: ``False``)
805
+ """
806
+ for attachment in self.attachments:
807
+ if not (skipHidden and attachment.hidden):
808
+ attachment.save(skipHidden = skipHidden, **kwargs)
809
+
810
+ def saveRaw(self, path) -> None:
811
+ # Create a 'raw' folder.
812
+ path = pathlib.Path(path)
813
+ # Make the location.
814
+ os.makedirs(path, exist_ok = True)
815
+ # Create the zipfile.
816
+ path /= 'raw.zip'
817
+ if path.exists():
818
+ raise FileExistsError(f'File "{path}" already exists.')
819
+ with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as zfile:
820
+ # Loop through all the directories
821
+ for dir_ in self.listDir():
822
+ sysdir = '/'.join(dir_)
823
+ code = dir_[-1][-8:]
824
+ if constants.PROPERTIES.get(code):
825
+ sysdir += ' - ' + constants.PROPERTIES[code]
826
+
827
+ # Generate appropriate filename.
828
+ if dir_[-1].endswith('001E') or dir_[-1].endswith('001F'):
829
+ filename = 'contents.txt'
830
+ else:
831
+ filename = 'contents.bin'
832
+
833
+ # Save contents of directory.
834
+ with zfile.open(sysdir + '/' + filename, 'w') as f:
835
+ data = self.getStream(dir_)
836
+ # Specifically check for None. If this is bytes we still
837
+ # want to do this line.
838
+ if data is not None:
839
+ f.write(data)
840
+
841
+ @functools.cached_property
842
+ def areStringsUnicode(self) -> bool:
843
+ """
844
+ Whether the strings are Unicode encoded or not.
845
+ """
846
+ return (self.getPropertyVal('340D0003', 0) & 0x40000) != 0
847
+
848
+ @functools.cached_property
849
+ def attachments(self) -> Union[List[AttachmentBase], List[SignedAttachment]]:
850
+ """
851
+ A list of all attachments.
852
+ """
853
+ # Get the attachments.
854
+ attachmentDirs = []
855
+ for dir_ in self.listDir(False, True, False):
856
+ if dir_[0].startswith('__attach') and dir_[0] not in attachmentDirs:
857
+ attachmentDirs.append(dir_[0])
858
+
859
+ attachments = []
860
+
861
+ for attachmentDir in attachmentDirs:
862
+ attachments.append(self.initAttachmentFunc(self, attachmentDir))
863
+
864
+ self.__attachmentsReady = True
865
+
866
+ return attachments
867
+
868
+ @property
869
+ def attachmentsDelayed(self) -> bool:
870
+ """
871
+ Returns ``True`` if the attachment initialization was delayed.
872
+ """
873
+ return self.__attachmentsDelayed
874
+
875
+ @property
876
+ def attachmentsReady(self) -> bool:
877
+ """
878
+ Returns ``True`` if the attachments are ready to be used.
879
+ """
880
+ return self.__attachmentsReady
881
+
882
+ @functools.cached_property
883
+ def classified(self) -> bool:
884
+ """
885
+ Indicates whether the contents of this message are regarded as
886
+ classified information.
887
+ """
888
+ return bool(self.getNamedProp('85B5', ps.PSETID_COMMON))
889
+
890
+ @functools.cached_property
891
+ def classType(self) -> Optional[str]:
892
+ """
893
+ The class type of the MSG file.
894
+ """
895
+ return self.getStringStream('__substg1.0_001A')
896
+
897
+ @functools.cached_property
898
+ def commonEnd(self) -> Optional[datetime.datetime]:
899
+ """
900
+ The end time for the object.
901
+ """
902
+ return self.getNamedProp('8517', ps.PSETID_COMMON)
903
+
904
+ @functools.cached_property
905
+ def commonStart(self) -> Optional[datetime.datetime]:
906
+ """
907
+ The start time for the object.
908
+ """
909
+ return self.getNamedProp('8516', ps.PSETID_COMMON)
910
+
911
+ @functools.cached_property
912
+ def contactLinkEntry(self) -> Optional[ContactLinkEntry]:
913
+ """
914
+ A class that contains the list of Address Book EntryIDs linked to this
915
+ Message object.
916
+ """
917
+ return self.getNamedAs('8585', ps.PSETID_COMMON, ContactLinkEntry)
918
+
919
+ @functools.cached_property
920
+ def contacts(self) -> Optional[List[str]]:
921
+ """
922
+ Contains the display name property of each Address Book EntryID
923
+ referenced in the value of the contactLinkEntry property.
924
+ """
925
+ return self.getNamedProp('853A', ps.PSETID_COMMON)
926
+
927
+ @functools.cached_property
928
+ def currentVersion(self) -> Optional[int]:
929
+ """
930
+ Specifies the build number of the client application that sent the
931
+ message.
932
+ """
933
+ return self.getNamedProp('8552', ps.PSETID_COMMON)
934
+
935
+ @functools.cached_property
936
+ def currentVersionName(self) -> Optional[str]:
937
+ """
938
+ Specifies the name of the client application that sent the message.
939
+ """
940
+ return self.getNamedProp('8554', ps.PSETID_COMMON)
941
+
942
+ @property
943
+ def dateFormat(self) -> str:
944
+ """
945
+ The format string to use when converting dates to strings.
946
+
947
+ This is used for dates with no time component.
948
+ """
949
+ return self.__dateFormat
950
+
951
+ @property
952
+ def datetimeFormat(self) -> str:
953
+ """
954
+ The format string to use when converting datetimes to strings.
955
+
956
+ This is used for dates that have time components.
957
+ """
958
+ return self.__dtFormat
959
+
960
+ @property
961
+ def errorBehavior(self) -> ErrorBehavior:
962
+ """
963
+ The behavior to follow when certain errors occur.
964
+
965
+ Will be an instance of the ErrorBehavior enum.
966
+ """
967
+ return self.__errorBehavior
968
+
969
+ @functools.cached_property
970
+ def importance(self) -> Optional[Importance]:
971
+ """
972
+ The specified importance of the MSG file.
973
+ """
974
+ return self.getPropertyAs('00170003', Importance)
975
+
976
+ @property
977
+ def importanceString(self) -> Union[str, None]:
978
+ """
979
+ The importance string to use for saving.
980
+
981
+ If the importance is medium then it returns ``None``. Mainly used for
982
+ saving.
983
+ """
984
+ return {
985
+ Importance.HIGH: 'High',
986
+ Importance.MEDIUM: None,
987
+ Importance.LOW: 'Low',
988
+ None: None,
989
+ }[self.importance]
990
+
991
+ @property
992
+ def initAttachmentFunc(self) -> Callable[[MSGFile, str], AttachmentBase]:
993
+ """
994
+ The method for initializing attachments being used, should you need to
995
+ use it externally for whatever reason.
996
+ """
997
+ return self.__initAttachmentFunc
998
+
999
+ @property
1000
+ def insecureFeatures(self) -> InsecureFeatures:
1001
+ """
1002
+ An enum specifying what insecure features have been enabled for this
1003
+ file.
1004
+ """
1005
+ return self.__inscFeat
1006
+
1007
+ @property
1008
+ def kwargs(self) -> Dict[str, Any]:
1009
+ """
1010
+ The kwargs used to initialize this message, excluding the prefix.
1011
+
1012
+ This is used for initializing embedded MSG files.
1013
+ """
1014
+ return self.__kwargs
1015
+
1016
+ @functools.cached_property
1017
+ def named(self) -> Named:
1018
+ """
1019
+ The main named properties storage.
1020
+
1021
+ This is not usable to access the data of the properties directly.
1022
+
1023
+ :raises ReferenceError: The parent ``MSGFile`` instance has been garbage
1024
+ collected.
1025
+ """
1026
+ # Handle the parent MSG file existing.
1027
+ if self.__parentMsg:
1028
+ # Try to get the named properties and use that for our main
1029
+ # instance.
1030
+ if (msg := self.__parentMsg()) is None:
1031
+ raise ReferenceError('Parent MSGFile instance has been garbage collected.')
1032
+ return msg.named
1033
+ else:
1034
+ return Named(self)
1035
+
1036
+ @functools.cached_property
1037
+ def namedProperties(self) -> NamedProperties:
1038
+ """
1039
+ The NamedProperties instances usable to access the data for named
1040
+ properties.
1041
+ """
1042
+ return NamedProperties(self.named, self)
1043
+
1044
+ @property
1045
+ def overrideEncoding(self) -> Optional[str]:
1046
+ """
1047
+ ``None`` if the encoding has not been overridden, otherwise the encoding
1048
+ used for string streams.
1049
+ """
1050
+ return self.__overrideEncoding
1051
+
1052
+ @property
1053
+ def path(self):
1054
+ """
1055
+ The message path if generated from a file, otherwise the data used to
1056
+ generate the ``MSGFile`` instance.
1057
+ """
1058
+ return self.__path
1059
+
1060
+ @property
1061
+ def prefix(self) -> str:
1062
+ """
1063
+ The prefix of the ``MSGFile`` instance.
1064
+
1065
+ Intended for developer use.
1066
+ """
1067
+ return self.__prefix
1068
+
1069
+ @property
1070
+ def prefixLen(self) -> int:
1071
+ """
1072
+ The number of elements in the prefix.
1073
+
1074
+ Dividing by 2 will typically tell you how deeply nested the MSG file is.
1075
+ """
1076
+ return self.__prefixLen
1077
+
1078
+ @property
1079
+ def prefixList(self) -> List[str]:
1080
+ """
1081
+ The prefix list of the Message instance.
1082
+
1083
+ Intended for developer use.
1084
+ """
1085
+ return copy.deepcopy(self.__prefixList)
1086
+
1087
+ @functools.cached_property
1088
+ def priority(self) -> Optional[Priority]:
1089
+ """
1090
+ The specified priority of the MSG file.
1091
+ """
1092
+ return self.getPropertyAs('00260003', Priority)
1093
+
1094
+ @functools.cached_property
1095
+ def props(self) -> PropertiesStore:
1096
+ """
1097
+ The ``PropertiesStore`` instance used by the ``MSGFile`` instance.
1098
+ """
1099
+ if not (stream := self.getStream('__properties_version1.0')):
1100
+ if ErrorBehavior.STANDARDS_VIOLATION in self.__errorBehavior:
1101
+ logger.error('File does not contain a property stream.')
1102
+ else:
1103
+ # Raise the exception from None so we don't get all the "during
1104
+ # the handling of the above exception" stuff.
1105
+ raise StandardViolationError('File does not contain a property stream.') from None
1106
+ return PropertiesStore(stream,
1107
+ PropertiesType.MESSAGE if not self.prefix else PropertiesType.MESSAGE_EMBED)
1108
+
1109
+ @functools.cached_property
1110
+ def retentionDate(self) -> Optional[datetime.datetime]:
1111
+ """
1112
+ The date, in UTC, after which a Message Object is expired by the server.
1113
+
1114
+ If ``None``, the Message object never expires.
1115
+ """
1116
+ return self.getPropertyVal('301C0040')
1117
+
1118
+ @functools.cached_property
1119
+ def retentionFlags(self) -> Optional[RetentionFlags]:
1120
+ """
1121
+ Flags that specify the status or nature of an item's retention tag or
1122
+ archive tag.
1123
+ """
1124
+ return self.getPropertyAs('301D0003', RetentionFlags)
1125
+
1126
+ @functools.cached_property
1127
+ def sensitivity(self) -> Optional[Sensitivity]:
1128
+ """
1129
+ The specified sensitivity of the MSG file.
1130
+ """
1131
+ return self.getPropertyAs('00360003', Sensitivity)
1132
+
1133
+ @functools.cached_property
1134
+ def sideEffects(self) -> Optional[SideEffect]:
1135
+ """
1136
+ Controls how a Message object is handled by the client in relation to
1137
+ certain user interface actions by the user, such as deleting a message.
1138
+ """
1139
+ return self.getNamedAs('8510', ps.PSETID_COMMON, SideEffect)
1140
+
1141
+ @property
1142
+ def stringEncoding(self) -> str:
1143
+ try:
1144
+ return self.__stringEncoding
1145
+ except AttributeError:
1146
+ # We need to calculate the encoding.
1147
+ # Let's first check if the encoding will be Unicode:
1148
+ if self.areStringsUnicode:
1149
+ self.__stringEncoding = "utf-16-le"
1150
+ else:
1151
+ # Well, it's not Unicode. Now we have to figure out what it IS.
1152
+ if '3FFD0003' not in self.props:
1153
+ # If this property is not set by the client, we SHOULD set
1154
+ # it to ISO-8859-15, but MAY set it to ISO-8859-1.
1155
+ logger.warning('Encoding property not found. Defaulting to ISO-8859-15.')
1156
+ self.__stringEncoding = 'iso-8859-15'
1157
+ else:
1158
+ enc = cast(int, self.getPropertyVal('3FFD0003'))
1159
+ # Now we just need to translate that value.
1160
+ self.__stringEncoding = lookupCodePage(enc)
1161
+ return self.__stringEncoding
1162
+
1163
+ @property
1164
+ def treePath(self) -> List[weakref.ReferenceType[Any]]:
1165
+ """
1166
+ A path, as a list of weak reference to the instances needed to get to
1167
+ this instance through the MSGFile-Attachment tree.
1168
+
1169
+ These are weak references to ensure the garbage collector doesn't see
1170
+ the references back to higher objects.
1171
+ """
1172
+ return self.__treePath