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,730 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'AttachmentBase',
6
+ ]
7
+
8
+
9
+ import abc
10
+ import datetime
11
+ import functools
12
+ import logging
13
+ import os
14
+ import pathlib
15
+ import weakref
16
+
17
+ from functools import cached_property
18
+ from typing import (
19
+ Any, List, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, Union
20
+ )
21
+
22
+ from ..constants import MSG_PATH, OVERRIDE_CLASS, SAVE_TYPE
23
+ from ..enums import AttachmentType
24
+ from ..properties.named import NamedProperties
25
+ from ..properties.properties_store import PropertiesStore
26
+ from ..utils import (
27
+ msgPathToString, tryGetMimetype, verifyPropertyId, verifyType
28
+ )
29
+
30
+
31
+ # Allow for nice type checking.
32
+ if TYPE_CHECKING:
33
+ from ..msg_classes.msg import MSGFile
34
+
35
+ logger = logging.getLogger(__name__)
36
+ logger.addHandler(logging.NullHandler())
37
+
38
+ _T = TypeVar('_T')
39
+
40
+
41
+ class AttachmentBase(abc.ABC):
42
+ """
43
+ The base class for all standard Attachments used by the module.
44
+ """
45
+
46
+ def __init__(self, msg: MSGFile, dir_: str, propStore: PropertiesStore):
47
+ """
48
+ :param msg: the Message instance that the attachment belongs to.
49
+ :param dir_: the directory inside the MSG file where the attachment is
50
+ located.
51
+ :param propStore: The PropertiesStore instance for the attachment.
52
+ """
53
+ self.__msg = weakref.ref(msg)
54
+ self.__dir = dir_
55
+ self.__props = propStore
56
+ self.__namedProperties = NamedProperties(msg.named, self)
57
+ self.__treePath = msg.treePath + [weakref.ref(self)]
58
+
59
+ def _getTypedAs(self, _id: str, overrideClass = None, preserveNone: bool = True):
60
+ """
61
+ Like the other get as functions, but designed for when something
62
+ could be multiple types (where only one will be present).
63
+
64
+ This way you have no need to set the type, it will be handled for you.
65
+
66
+ :param overrideClass: Class/function to use to morph the data that was
67
+ read. The data will be the first argument to the class's
68
+ ``__init__`` method or the function itself, if that is what is
69
+ provided. By default, this will be completely ignored if the value
70
+ was not found.
71
+ :param preserveNone: If ``True`` (default), causes the function to
72
+ ignore :param overrideClass: when the value could not be found (is
73
+ ``None``). If this is changed to ``False``, then the value will be
74
+ used regardless.
75
+ """
76
+ value = self._getTypedData(_id)
77
+ # Check if we should be overriding the data type for this instance.
78
+ if overrideClass is not None:
79
+ if value is not None or not preserveNone:
80
+ value = overrideClass(value)
81
+
82
+ return value
83
+
84
+ def _getTypedData(self, id, _type = None):
85
+ """
86
+ Gets the data for the specified id as the type that it is supposed to
87
+ be.
88
+
89
+ :param id: MUST be a 4 digit hexadecimal string.
90
+
91
+ If you know for sure what type the data is before hand, you can specify
92
+ it as being one of the strings in the constant
93
+ ``FIXED_LENGTH_PROPS_STRING`` or ``VARIABLE_LENGTH_PROPS_STRING``.
94
+
95
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
96
+ garbage collected.
97
+ """
98
+ verifyPropertyId(id)
99
+ id = id.upper()
100
+ found, result = self._getTypedStream('__substg1.0_' + id, _type)
101
+ if found:
102
+ return result
103
+ else:
104
+ found, result = self._getTypedProperty(id, _type)
105
+ return result if found else None
106
+
107
+ def _getTypedProperty(self, propertyID, _type = None) -> Tuple[bool, Optional[object]]:
108
+ """
109
+ Gets the property with the specified id as the type that it is supposed
110
+ to be.
111
+
112
+ :param id: MUST be a 4 digit hexadecimal string.
113
+
114
+ If you know for sure what type the property is before hand, you can
115
+ specify it as being one of the strings in the constant
116
+ ``FIXED_LENGTH_PROPS_STRING`` or ``VARIABLE_LENGTH_PROPS_STRING``.
117
+ """
118
+ verifyPropertyId(propertyID)
119
+ if _type:
120
+ verifyType(_type)
121
+ propertyID += _type
122
+
123
+ notFound = object()
124
+ ret = self.getPropertyVal(propertyID, notFound)
125
+ if ret is notFound:
126
+ return False, None
127
+
128
+ return True, ret
129
+
130
+ def _getTypedStream(self, filename: MSG_PATH, _type = None):
131
+ """
132
+ Gets the contents of the specified stream as the type that it is
133
+ supposed to be.
134
+
135
+ Rather than the full filename, you should only feed this function the
136
+ filename sans the type. So if the full name is "__substg1.0_001A001F",
137
+ the filename this function should receive should be "__substg1.0_001A".
138
+
139
+ If you know for sure what type the stream is before hand, you can
140
+ specify it as being one of the strings in the constant
141
+ ``FIXED_LENGTH_PROPS_STRING`` or ``VARIABLE_LENGTH_PROPS_STRING``.
142
+
143
+ If you have not specified the type, the type this method returns in many
144
+ cases cannot be predicted. As such, when using this method it is best
145
+ for you to check the type that it returns. If the function returns
146
+ ``None``, that means it could not find the stream specified.
147
+
148
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
149
+ garbage collected.
150
+ """
151
+ if (msg := self.__msg()) is None:
152
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
153
+ return msg._getTypedStream([self.__dir, msgPathToString(filename)], True, _type)
154
+
155
+ def _handleFnc(self, _zip, filename, customPath, kwargs) -> pathlib.Path:
156
+ """
157
+ "Handle Filename Conflict"
158
+
159
+ Internal function for use in determining how to modify the saving path
160
+ when a file with the same name already exists. This is mainly because
161
+ any save function that uses files will need to do this functionality.
162
+
163
+ :returns: A ``pathlib.Path`` object to where the file should be saved.
164
+ """
165
+ fullFilename = customPath / filename
166
+
167
+ overwriteExisting = kwargs.get('overwriteExisting', False)
168
+
169
+ if _zip:
170
+ # If we are writing to a zip file and are not overwriting.
171
+ if not overwriteExisting:
172
+ name, ext = os.path.splitext(filename)
173
+ nameList = _zip.namelist()
174
+ if str(fullFilename).replace('\\', '/') in nameList:
175
+ for i in range(2, 100):
176
+ testName = customPath / f'{name} ({i}){ext}'
177
+ if str(testName).replace('\\', '/') not in nameList:
178
+ return testName
179
+ else:
180
+ # If we couldn't find one that didn't exist.
181
+ raise FileExistsError(f'Could not create the specified file because it already exists ("{fullFilename}").')
182
+ else:
183
+ if not overwriteExisting and fullFilename.exists():
184
+ # Try to split the filename into a name and extension.
185
+ name, ext = os.path.splitext(filename)
186
+ # Try to add a number to it so that we can save without overwriting.
187
+ for i in range(2, 100):
188
+ testName = customPath / f'{name} ({i}){ext}'
189
+ if not testName.exists():
190
+ return testName
191
+ else:
192
+ # If we couldn't find one that didn't exist.
193
+ raise FileExistsError(f'Could not create the specified file because it already exists ("{fullFilename}").')
194
+
195
+ return fullFilename
196
+
197
+ def exists(self, filename: MSG_PATH) -> bool:
198
+ """
199
+ Checks if stream exists inside the attachment folder.
200
+
201
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
202
+ garbage collected.
203
+ """
204
+ if (msg := self.__msg()) is None:
205
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
206
+ return msg.exists([self.__dir, msgPathToString(filename)])
207
+
208
+ def sExists(self, filename: MSG_PATH) -> bool:
209
+ """
210
+ Checks if the string stream exists inside the attachment folder.
211
+
212
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
213
+ garbage collected.
214
+ """
215
+ if (msg := self.__msg()) is None:
216
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
217
+ return msg.sExists([self.__dir, msgPathToString(filename)])
218
+
219
+ def existsTypedProperty(self, id, _type = None) -> Tuple[bool, int]:
220
+ """
221
+ Determines if the stream with the provided id exists.
222
+
223
+ The return of this function is 2 values, the first being a ``bool`` for
224
+ if anything was found, and the second being how many were found.
225
+
226
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
227
+ garbage collected.
228
+ """
229
+ if (msg := self.__msg()) is None:
230
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
231
+ return msg.existsTypedProperty(id, self.__dir, _type, True, self.__props)
232
+
233
+ @abc.abstractmethod
234
+ def getFilename(self, **kwargs) -> str:
235
+ """
236
+ Returns the filename to use for the attachment.
237
+
238
+ :param contentId: Use the contentId, if available.
239
+ :param customFilename: A custom name to use for the file.
240
+
241
+ If the filename starts with "UnknownFilename" then there is no guarantee
242
+ that the files will have exactly the same filename.
243
+ """
244
+
245
+ def getMultipleBinary(self, filename: MSG_PATH) -> Optional[List[bytes]]:
246
+ """
247
+ Gets a multiple binary property as a list of ``bytes`` objects.
248
+
249
+ Like :meth:`getStringStream`, the 4 character type suffix should be
250
+ omitted. So if you want the stream "__substg1.0_00011102" then the
251
+ filename would simply be "__substg1.0_0001".
252
+
253
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
254
+ garbage collected.
255
+ """
256
+ if (msg := self.__msg()) is None:
257
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
258
+ return msg.getMultipleBinary([self.__dir, msgPathToString(filename)])
259
+
260
+ def getMultipleString(self, filename: MSG_PATH) -> Optional[List[str]]:
261
+ """
262
+ Gets a multiple string property as a list of ``str`` objects.
263
+
264
+ Like :meth:`getStringStream`, the 4 character type suffix should be
265
+ omitted. So if you want the stream "__substg1.0_00011102" then the
266
+ filename would simply be "__substg1.0_0001".
267
+
268
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
269
+ garbage collected.
270
+ """
271
+ if (msg := self.__msg()) is None:
272
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
273
+ return msg.getMultipleString([self.__dir, msgPathToString(filename)])
274
+
275
+ def getNamedAs(self, propertyName: str, guid: str, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
276
+ """
277
+ Returns the named property, setting the class if specified.
278
+
279
+ :param overrideClass: Class/function to use to morph the data that was
280
+ read. The data will be the first argument to the class's
281
+ ``__init__`` method or the function itself, if that is what is
282
+ provided. If the value is ``None``, this function is not called. If
283
+ you want it to be called regardless, you should handle the data
284
+ directly.
285
+ """
286
+ value = self.getNamedProp(propertyName, guid)
287
+ if value is not None:
288
+ value = overrideClass(value)
289
+ return value
290
+
291
+ def getNamedProp(self, propertyName: str, guid: str, default: _T = None) -> Union[Any, _T]:
292
+ """
293
+ ``instance.namedProperties.get((propertyName, guid), default)``
294
+
295
+ Can be overriden to create new behavior.
296
+ """
297
+ return self.namedProperties.get((propertyName, guid), default)
298
+
299
+ def getPropertyAs(self, propertyName: Union[int, str], overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
300
+ """
301
+ Returns the property, setting the class if found.
302
+
303
+ :param overrideClass: Class/function to use to morph the data that was
304
+ read. The data will be the first argument to the class's
305
+ ``__init__`` method or the function itself, if that is what is
306
+ provided. If the value is ``None``, this function is not called. If
307
+ you want it to be called regardless, you should handle the data
308
+ directly.
309
+ """
310
+ value = self.getPropertyVal(propertyName)
311
+
312
+ if value is not None:
313
+ value = overrideClass(value)
314
+
315
+ return value
316
+
317
+ def getPropertyVal(self, name: Union[int, str], default: _T = None) -> Union[Any, _T]:
318
+ """
319
+ instance.props.getValue(name, default)
320
+
321
+ Can be overridden to create new behavior.
322
+ """
323
+ return self.props.getValue(name, default)
324
+
325
+ def getSingleOrMultipleBinary(self, filename: MSG_PATH) -> Optional[Union[List[bytes], bytes]]:
326
+ """
327
+ A combination of :meth:`getStringStream` and :meth:`getMultipleString`.
328
+
329
+ Checks to see if a single binary stream exists to return, otherwise
330
+ tries to return the multiple binary stream of the same ID.
331
+
332
+ Like :meth:`getStringStream`, the 4 character type suffix should be
333
+ omitted. So if you want the stream "__substg1.0_00010102" then the
334
+ filename would simply be "__substg1.0_0001".
335
+
336
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
337
+ garbage collected.
338
+ """
339
+ if (msg := self.__msg()) is None:
340
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
341
+ return msg.getSingleOrMultipleBinary([self.__dir, msgPathToString(filename)])
342
+
343
+ def getSingleOrMultipleString(self, filename: MSG_PATH) -> Optional[Union[List[str], str]]:
344
+ """
345
+ A combination of :meth:`getStringStream` and :meth:`getMultipleString`.
346
+
347
+ Checks to see if a single string stream exists to return, otherwise
348
+ tries to return the multiple string stream of the same ID.
349
+
350
+ Like :meth:`getStringStream`, the 4 character type suffix should be
351
+ omitted. So if you want the stream "__substg1.0_0001001F" then the
352
+ filename would simply be "__substg1.0_0001".
353
+
354
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
355
+ garbage collected.
356
+ """
357
+ if (msg := self.__msg()) is None:
358
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
359
+ return msg.getSingleOrMultipleString([self.__dir, msgPathToString(filename)])
360
+
361
+ def getStream(self, filename: MSG_PATH) -> Optional[bytes]:
362
+ """
363
+ Gets a binary representation of the requested stream.
364
+
365
+ This should ALWAYS return a ``bytes`` object if it was found, otherwise
366
+ returns ``None``.
367
+
368
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
369
+ garbage collected.
370
+ """
371
+ if (msg := self.__msg()) is None:
372
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
373
+ return msg.getStream([self.__dir, msgPathToString(filename)])
374
+
375
+ def getStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
376
+ """
377
+ Returns the specified stream, modifying it to the specified class if it
378
+ is found.
379
+
380
+ :param overrideClass: Class/function to use to morph the data that was
381
+ read. The data will be the first argument to the class's
382
+ ``__init__`` method or the function itself, if that is what is
383
+ provided. If the value is ``None``, this function is not called. If
384
+ you want it to be called regardless, you should handle the data
385
+ directly.
386
+ """
387
+ value = self.getStream(streamID)
388
+
389
+ if value is not None:
390
+ value = overrideClass(value)
391
+
392
+ return value
393
+
394
+ def getStringStream(self, filename: MSG_PATH) -> Optional[str]:
395
+ """
396
+ Gets a string representation of the requested stream.
397
+
398
+ Rather than the full filename, you should only feed this function the
399
+ filename sans the type. So if the full name is "__substg1.0_001A001F",
400
+ the filename this function should receive should be "__substg1.0_001A".
401
+
402
+ This should ALWAYS return a string if it was found, otherwise returns
403
+ ``None``.
404
+
405
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
406
+ garbage collected.
407
+ """
408
+ if (msg := self.__msg()) is None:
409
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
410
+ return msg.getStringStream([self.__dir, msgPathToString(filename)])
411
+
412
+ def getStringStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
413
+ """
414
+ Returns the specified string stream, modifying it to the specified
415
+ class if it is found.
416
+
417
+ :param overrideClass: Class/function to use to morph the data that was
418
+ read. The data will be the first argument to the class's
419
+ ``__init__`` method or the function itself, if that is what is
420
+ provided. If the value is ``None``, this function is not called. If
421
+ you want it to be called regardless, you should handle the data
422
+ directly.
423
+ """
424
+ value = self.getStream(streamID)
425
+
426
+ if value is not None:
427
+ value = overrideClass(value)
428
+
429
+ return value
430
+
431
+ def listDir(self, streams: bool = True, storages: bool = False) -> List[List[str]]:
432
+ """
433
+ Lists the streams and/or storages that exist in the attachment
434
+ directory.
435
+
436
+ Returns the paths *excluding* the attachment directory, allowing the
437
+ paths to be directly used for accessing a stream.
438
+ """
439
+ if (msg := self.__msg()) is None:
440
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
441
+ return [path[1:] for path in msg.listDir(streams, storages, False)
442
+ if len(path) > 1 and path[0] == self.__dir]
443
+
444
+ def slistDir(self, streams: bool = True, storages: bool = False) -> List[str]:
445
+ """
446
+ Like listDir, except it returns the paths as strings.
447
+ """
448
+ return ['/'.join(path) for path in self.listDir(streams, storages)]
449
+
450
+ @abc.abstractmethod
451
+ def save(self, **kwargs) -> SAVE_TYPE:
452
+ """
453
+ Saves the attachment data.
454
+
455
+ The name of the file is determined by the logic of :meth:`getFilename`.
456
+ If you are a developer, ensure that you use this behavior.
457
+
458
+ To change the directory that the attachment is saved to, set the value
459
+ of :param customPath: when calling this function. The default save
460
+ directory is the working directory.
461
+
462
+ If you want to save the contents into a ``ZipFile`` or similar object,
463
+ either pass a path to where you want to create one or pass an instance
464
+ to :param zip:. If :param zip: is an instance, :param customPath: will
465
+ refer to a location inside the zip file.
466
+
467
+ :param extractEmbedded: If ``True``, causes the attachment, should it
468
+ be an embedded MSG file, to save as a .msg file instead of calling its save function.
469
+ :param skipEmbedded: If ``True``, skips saving this attachment if it is
470
+ an embedded MSG file.
471
+
472
+ :returns: A tuple that specifies how the data was saved. The value of
473
+ the first item specifies what the second value will be.
474
+ """
475
+
476
+ @cached_property
477
+ def attachmentEncoding(self) -> Optional[bytes]:
478
+ """
479
+ The encoding information about the attachment object.
480
+
481
+ Will return ``b'\\x2A\\x86\\x48\\x86\\xf7\\x14\\x03\\x0b\\x01'`` if
482
+ encoded in MacBinary format, otherwise it is unset.
483
+ """
484
+ return self.getStream('__substg1.0_37020102')
485
+
486
+ @cached_property
487
+ def additionalInformation(self) -> Optional[str]:
488
+ """
489
+ The additional information about the attachment.
490
+
491
+ This property MUST be an empty string if attachmentEncoding is not set.
492
+ Otherwise it MUST be set to a string of the format ":CREA:TYPE" where
493
+ ":CREA" is the four-letter Macintosh file creator code and ":TYPE" is a
494
+ four-letter Macintosh type code.
495
+ """
496
+ return self.getStringStream('__substg1.0_370F')
497
+
498
+ @cached_property
499
+ def cid(self) -> Optional[str]:
500
+ """
501
+ Returns the Content ID of the attachment, if it exists.
502
+ """
503
+ return self.getStringStream('__substg1.0_3712')
504
+
505
+ @cached_property
506
+ def clsid(self) -> str:
507
+ """
508
+ Returns the CLSID for the data stream/storage of the attachment.
509
+ """
510
+ # Set some default values.
511
+ clsid = '00000000-0000-0000-0000-000000000000'
512
+ dataStream = None
513
+
514
+ if self.exists('__substg1.0_3701000D'):
515
+ dataStream = [self.__dir, '__substg1.0_3701000D']
516
+ elif self.exists('__substg1.0_37010102'):
517
+ dataStream = [self.__dir, '__substg1.0_37010102']
518
+
519
+ # If we found the right item, get the CLSID.
520
+ if dataStream:
521
+ if (msg := self.__msg()) is None:
522
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
523
+ clsid = msg._getOleEntry(dataStream).clsid or clsid
524
+
525
+ return clsid
526
+
527
+ @property
528
+ def contentId(self) -> Optional[str]:
529
+ """
530
+ Alias of :attr:`cid`.
531
+ """
532
+ return self.cid
533
+
534
+ @property
535
+ def createdAt(self) -> Optional[datetime.datetime]:
536
+ """
537
+ Alias of :attr:`creationTime`.
538
+ """
539
+ return self.creationTime
540
+
541
+ @cached_property
542
+ def creationTime(self) -> Optional[datetime.datetime]:
543
+ """
544
+ The time the attachment was created.
545
+ """
546
+ return self.getPropertyVal('30070040')
547
+
548
+ @property
549
+ @abc.abstractmethod
550
+ def data(self) -> Optional[object]:
551
+ """
552
+ The attachment data, if any.
553
+
554
+ Returns ``None`` if there is no data to save.
555
+ """
556
+
557
+ @cached_property
558
+ def dataType(self) -> Optional[Type[object]]:
559
+ """
560
+ The class that the data type will use, if it can be retrieved.
561
+
562
+ This is a safe way to do type checking on data before knowing if it will
563
+ raise an exception. Returns ``None`` if no data will be returned or if
564
+ an exception will be raised.
565
+ """
566
+ try:
567
+ return None if self.data is None else self.data.__class__
568
+ except Exception:
569
+ # All exceptions that accessing data would cause should be silenced.
570
+ return None
571
+
572
+ @property
573
+ def dir(self) -> str:
574
+ """
575
+ Returns the directory inside the MSG file where the attachment is
576
+ located.
577
+ """
578
+ return self.__dir
579
+
580
+ @cached_property
581
+ def displayName(self) -> Optional[str]:
582
+ """
583
+ Returns the display name of the folder.
584
+ """
585
+ return self.getStringStream('__substg1.0_3001')
586
+
587
+ @cached_property
588
+ def exceptionReplaceTime(self) -> Optional[datetime.datetime]:
589
+ """
590
+ The original date and time at which the instance in the recurrence
591
+ pattern would have occurred if it were not an exception.
592
+
593
+ Only applicable if the attachment is an Exception object.
594
+ """
595
+ return self.getPropertyVal('7FF90040')
596
+
597
+ @cached_property
598
+ def extension(self) -> Optional[str]:
599
+ """
600
+ The reported extension for the file.
601
+ """
602
+ return self.getStringStream('__substg1.0_3703')
603
+
604
+ @cached_property
605
+ def hidden(self) -> bool:
606
+ """
607
+ Indicates whether an Attachment object is hidden from the end user.
608
+ """
609
+ return bool(self.getPropertyVal('7FFE000B'))
610
+
611
+ @cached_property
612
+ def isAttachmentContactPhoto(self) -> bool:
613
+ """
614
+ Whether the attachment is a contact photo for a Contact object.
615
+ """
616
+ return bool(self.getPropertyVal('7FFF000B'))
617
+
618
+ @cached_property
619
+ def lastModificationTime(self) -> Optional[datetime.datetime]:
620
+ """
621
+ The last time the attachment was modified.
622
+ """
623
+ return self.getPropertyVal('30080040')
624
+
625
+ @cached_property
626
+ def longFilename(self) -> Optional[str]:
627
+ """
628
+ Returns the long file name of the attachment, if it exists.
629
+ """
630
+ return self.getStringStream('__substg1.0_3707')
631
+
632
+ @cached_property
633
+ def longPathname(self) -> Optional[str]:
634
+ """
635
+ The fully qualified path and file name with extension.
636
+ """
637
+ return self.getStringStream('__substg1.0_370D')
638
+
639
+ @cached_property
640
+ def mimetype(self) -> Optional[str]:
641
+ """
642
+ The content-type mime header of the attachment, if specified.
643
+ """
644
+ return tryGetMimetype(self, self.getStringStream('__substg1.0_370E'))
645
+
646
+ @property
647
+ def modifiedAt(self) -> Optional[datetime.datetime]:
648
+ """
649
+ Alias of :attr:`lastModificationTime`.
650
+ """
651
+ return self.lastModificationTime
652
+
653
+ @property
654
+ def msg(self) -> MSGFile:
655
+ """
656
+ Returns the ``MSGFile`` instance the attachment belongs to.
657
+
658
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
659
+ garbage collected.
660
+ """
661
+ if (msg := self.__msg()) is None:
662
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
663
+ return msg
664
+
665
+ @cached_property
666
+ def name(self) -> Optional[str]:
667
+ """
668
+ The best name available for the file.
669
+
670
+ Uses long filename before short.
671
+ """
672
+ if self.type is AttachmentType.MSG:
673
+ if self.displayName:
674
+ return self.displayName + '.msg'
675
+ return self.longFilename or self.shortFilename
676
+
677
+ @property
678
+ def namedProperties(self) -> NamedProperties:
679
+ """
680
+ The NamedAttachmentProperties instance for this attachment.
681
+ """
682
+ return self.__namedProperties
683
+
684
+ @cached_property
685
+ def payloadClass(self) -> Optional[str]:
686
+ """
687
+ The class name of an object that can display the contents of the
688
+ message.
689
+ """
690
+ return self.getStringStream('__substg1.0_371A')
691
+
692
+ @property
693
+ def props(self) -> PropertiesStore:
694
+ """
695
+ Returns the Properties instance of the attachment.
696
+ """
697
+ return self.__props
698
+
699
+ @cached_property
700
+ def renderingPosition(self) -> Optional[int]:
701
+ """
702
+ The offset, in rendered characters, to use when rendering the attachment
703
+ within the main message text.
704
+
705
+ A value of ``0xFFFFFFFF`` indicates a hidden attachment that is not to
706
+ be rendered.
707
+ """
708
+ return self.getPropertyVal('370B0003')
709
+
710
+ @cached_property
711
+ def shortFilename(self) -> Optional[str]:
712
+ """
713
+ The short file name of the attachment, if it exists.
714
+ """
715
+ return self.getStringStream('__substg1.0_3704')
716
+
717
+ @property
718
+ def treePath(self) -> List[weakref.ReferenceType[Any]]:
719
+ """
720
+ A path, as a tuple of instances, needed to get to this instance through
721
+ the MSGFile-Attachment tree.
722
+ """
723
+ return self.__treePath
724
+
725
+ @property
726
+ @abc.abstractmethod
727
+ def type(self) -> AttachmentType:
728
+ """
729
+ An enum value that identifies the type of attachment.
730
+ """