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,476 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'PropertiesStore',
6
+ ]
7
+
8
+
9
+ import copy
10
+ import datetime
11
+ import logging
12
+ import pprint
13
+
14
+ from typing import (
15
+ Any, Dict, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union
16
+ )
17
+
18
+ from .. import constants
19
+ from ..enums import PropertiesType
20
+ from ..exceptions import NotWritableError
21
+ from .prop import createProp, FixedLengthProp, PropBase
22
+ from ..utils import divide
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+ logger.addHandler(logging.NullHandler())
27
+
28
+ _T = TypeVar('_T')
29
+
30
+
31
+ class PropertiesStore:
32
+ """
33
+ Parser for msg properties files.
34
+ """
35
+
36
+ def __init__(self, data: Optional[bytes], type_: PropertiesType, writable: bool = False):
37
+ """
38
+ Reads a properties stream or creates a brand new ``PropertiesStore``
39
+ object.
40
+
41
+ :param data: The bytes of the properties instance. Setting to ``None``
42
+ or empty bytes will cause the properties instance to not be valid
43
+ *unless* writable is set to ``True``. If that is the case, the
44
+ instance will be setup for creating a new properties stream.
45
+ :param type_: The type of properties stream this instance represents.
46
+ :param writable: Whether this properties stream should accept
47
+ modification.
48
+ """
49
+ if not isinstance(type_, PropertiesType):
50
+ raise TypeError(':param type_: MUST be a value of PropertiesType.')
51
+
52
+ self.__type = type_
53
+
54
+ # Setup early variables.
55
+ self.__props: Dict[str, PropBase] = {}
56
+ # This maps short IDs to all properties that use that ID. More than one
57
+ # property with the same ID but a different type may exist.
58
+ self.__idMapping: Dict[str, List[str]] = {}
59
+ self.__naid = None
60
+ self.__nrid = None
61
+ self.__ac = None
62
+ self.__rc = None
63
+ self.__writable = writable
64
+ # Set this now and unset it if everything goes well.
65
+ self.__isError = True
66
+
67
+ # Check if data is None or empty bytes.
68
+ if not data:
69
+ # Two paths here. If writable, we are just meant to be creating a
70
+ # new storage for properties, so initialize and return. Otherwise,
71
+ # we're dealing with an error situation, but we want to silence it.
72
+ if writable:
73
+ self.__rawData = b''
74
+ if type_ is not PropertiesType.ATTACHMENT:
75
+ self.__naid = 0
76
+ self.__nrid = 0
77
+ self.__ac = 0
78
+ self.__rc = 0
79
+ return
80
+
81
+ if not isinstance(data, bytes):
82
+ raise TypeError(':param data: MUST be bytes or None.')
83
+ self.__rawData = data
84
+
85
+ if type_ == PropertiesType.MESSAGE:
86
+ skip = 32
87
+ self.__nrid, self.__naid, self.__rc, self.__ac = constants.st.ST_PROPSTORE_HEADER.unpack(data[:24])
88
+ elif type_ == PropertiesType.MESSAGE_EMBED:
89
+ skip = 24
90
+ self.__nrid, self.__naid, self.__rc, self.__ac = constants.st.ST_PROPSTORE_HEADER.unpack(data[:24])
91
+ else:
92
+ skip = 8
93
+ streams = divide(self.__rawData[skip:], 16)
94
+ for st in streams:
95
+ if len(st) == 16:
96
+ prop = createProp(st)
97
+ self.__props[prop.name] = prop
98
+
99
+ # Add the ID to our mapping list.
100
+ id_ = prop.name[:4]
101
+ if id_ not in self.__idMapping:
102
+ self.__idMapping[id_] = []
103
+ self.__idMapping[id_].append(prop.name)
104
+ else:
105
+ logger.warning(f'Found stream from divide that was not 16 bytes: {st}. Ignoring.')
106
+ self.__isError = False
107
+
108
+ def __bytes__(self) -> bytes:
109
+ return self.toBytes()
110
+
111
+ def __contains__(self, key: Any) -> bool:
112
+ return self.__props.__contains__(key)
113
+
114
+ def __delitem__(self, key: str) -> None:
115
+ """
116
+ Removes an item using the del operator.
117
+
118
+ :raises KeyError: The key was not found.
119
+ :raises TypeError: The key was not a string.
120
+ """
121
+
122
+ if not isinstance(key, str):
123
+ raise TypeError('Del operator can only remove a property by string.')
124
+
125
+ key = key.upper()
126
+
127
+ del self.__props[key]
128
+
129
+ # If the deletion was successful, we need to remove the related ID
130
+ # mapping.
131
+ shortKey = key[:4]
132
+ self.__idMapping[shortKey].remove(key)
133
+ if len(self.__idMapping[shortKey]) == 0:
134
+ del self.__idMapping[shortKey]
135
+
136
+ def __getitem__(self, key: Union[str, int]) -> PropBase:
137
+ if (found := self._mapId(key)):
138
+ return self.__props.__getitem__(found)
139
+ raise KeyError(key)
140
+
141
+ def __iter__(self) -> Iterator[str]:
142
+ return self.__props.__iter__()
143
+
144
+ def __len__(self) -> int:
145
+ """
146
+ Returns the number of properties.
147
+ """
148
+ return len(self.__props)
149
+
150
+ def __repr__(self) -> str:
151
+ return self.__props.__repr__()
152
+
153
+ def _mapId(self, id_: Union[int, str]) -> str:
154
+ """
155
+ Converts an input into an appropriate property ID.
156
+
157
+ This is a complex function, allowing the user to specify an int or
158
+ string. If the input is a string that is not 4 characters, it is
159
+ returned. Otherwise, a series of checks will be
160
+ performed. If the input is an int that is less than ``0x10000``, it is
161
+ considered a property ID without a type and converted to a 4 character
162
+ hexadecimal string. Otherwise, it is converted to an 8 character
163
+ hexadecimal string and returned.
164
+
165
+ Once the input is a 4 character string from the other paths, it will
166
+ then be checked against the list of found property IDs, and the first
167
+ full ID will be returned.
168
+
169
+ If a valid conversion could not be done, returns an empty string.
170
+
171
+ All strings returned will be uppercase.
172
+ """
173
+ # See if we need to convert to 4 character string and map or if we just
174
+ # need to return quickly.
175
+ if isinstance(id_, str):
176
+ id_ = id_.upper()
177
+ if len(id_) != 4:
178
+ return id_
179
+ elif isinstance(id_, int):
180
+ if id_ >= 0x10000:
181
+ return f'{id_:08X}'
182
+ else:
183
+ id_ = f'{id_:04X}'
184
+ else:
185
+ return ''
186
+
187
+ return self.__idMapping.get(id_, ('',))[0]
188
+
189
+ def addProperty(self, prop: PropBase, force: bool = False) -> None:
190
+ """
191
+ Adds the property if it does not exist.
192
+
193
+ :param prop: The property to add.
194
+ :param force: If ``True``, the writable property will be ignored. This
195
+ will not be reflected when converting to ``bytes`` if the instance
196
+ is not readable.
197
+
198
+ :raises KeyError: A property already exists with the chosen name.
199
+ :raises NotWritableError: The method was used on an unwritable instance.
200
+ """
201
+ if not (force or self.__writable):
202
+ raise
203
+
204
+ if prop.name in self.__props:
205
+ raise KeyError('A property with that name already exists.')
206
+ self.__props[prop.name.upper()] = prop
207
+ self.__idMapping.setdefault(prop.name[:4], list()).append(prop.name.upper())
208
+
209
+ def get(self, name: Union[str, int], default: _T = None) -> Union[PropBase, _T]:
210
+ """
211
+ Retrieve the property of :param name:.
212
+
213
+ :returns: The property, or the value of :param default: if the property
214
+ could not be found.
215
+ """
216
+ if (name := self._mapId(name)):
217
+ return self.__props.get(name, default)
218
+ else:
219
+ return default
220
+
221
+ def getProperties(self, id_: Union[str, int]) -> List[PropBase]:
222
+ """
223
+ Gets all properties with the specified ID.
224
+
225
+ :param ID: An 4 digit hexadecimal string or an int that is less than
226
+ 0x10000.
227
+ """
228
+ if isinstance(id_, int):
229
+ if id_ >= 0x10000:
230
+ return []
231
+ else:
232
+ id_ = f'{id_:04X}'
233
+ elif isinstance(id_, str):
234
+ if len(id_) == 4:
235
+ id_ = id_.upper()
236
+ else:
237
+ return []
238
+
239
+ return [self[x] for x in self.__idMapping.get(id_, [])]
240
+
241
+ def getValue(self, name: Union[str, int], default: _T = None) -> Union[Any, _T]:
242
+ """
243
+ Attempts to get the first property
244
+ """
245
+ if isinstance(name, int):
246
+ if name >= 0x10000:
247
+ name = f'{name:08X}'
248
+ else:
249
+ name = f'{name:04X}'
250
+ if len(name) == 4:
251
+ for prop in self.getProperties(name):
252
+ if isinstance(prop, FixedLengthProp):
253
+ return prop.value
254
+ return default
255
+ elif len(name) == 8:
256
+ if (prop := self.get(name)):
257
+ if isinstance(prop, FixedLengthProp):
258
+ return prop.value
259
+ else:
260
+ return default
261
+ return default
262
+ else:
263
+ raise ValueError('Property name must be an int less than 0x100000000, a 4 character hex string, or an 8 character hex string.')
264
+
265
+ def items(self) -> Iterable[Tuple[str, PropBase]]:
266
+ return self.__props.items()
267
+
268
+ def keys(self) -> Iterable[str]:
269
+ return self.__props.keys()
270
+
271
+ def makeWritable(self) -> PropertiesStore:
272
+ """
273
+ Returns a copy of this PropertiesStore object that allows modification.
274
+
275
+ If the instance is already writable, this will return the object.
276
+ """
277
+ if self.__writable:
278
+ return self
279
+ return PropertiesStore(self.__rawData, self.__type, True)
280
+
281
+ def pprintKeys(self) -> None:
282
+ """
283
+ Uses the pprint function on a sorted list of the keys.
284
+ """
285
+ pprint.pprint(sorted(self.__props.keys()))
286
+
287
+ def removeProperty(self, nameOrProp: Union[str, PropBase]) -> None:
288
+ """
289
+ Removes the property by name or by instance.
290
+
291
+ Due to possible ambiguities, this function does *not* accept an int
292
+ argument nor will it be able to find a property based on the 4 character
293
+ hex ID.
294
+
295
+ :raises KeyError: The property was not found.
296
+ :raises NotWritableError: The instance is not writable.
297
+ :raises TypeError: The type for :param nameOrProp: was wrong.
298
+ """
299
+ if isinstance(nameOrProp, str):
300
+ del self[nameOrProp]
301
+ elif isinstance(nameOrProp, PropBase):
302
+ del self[nameOrProp.name]
303
+ else:
304
+ raise TypeError(f'Cannot remove property using type {type(nameOrProp)}.')
305
+
306
+ def toBytes(self) -> bytes:
307
+ if self.__writable:
308
+ # The reserved field is present on all of them.
309
+ ret = b'\x00' * 8
310
+
311
+ # Add additional fields depending on type.
312
+ if self.__type is not PropertiesType.ATTACHMENT:
313
+ ret += constants.st.ST_PROPSTORE_HEADER.pack(self.__nrid, self.__naid, self.__rc, self.__ac)
314
+ if self.__type is PropertiesType.MESSAGE:
315
+ ret += b'\x00' * 8
316
+
317
+ # Convert all the properties to bytes.
318
+ ret += b''.join(bytes(prop) for prop in self.__props.values())
319
+
320
+ return ret
321
+ else:
322
+ return self.__rawData
323
+
324
+ def values(self) -> Iterable[PropBase]:
325
+ return self.__props.values()
326
+
327
+ items.__doc__ = dict.items.__doc__
328
+ keys.__doc__ = dict.keys.__doc__
329
+ values.__doc__ = dict.values.__doc__
330
+
331
+ @property
332
+ def attachmentCount(self) -> int:
333
+ """
334
+ The number of Attachment objects for the ``MSGFile`` object.
335
+
336
+ :raises NotWritableError: The setter was used on an unwritable instance.
337
+ :raises TypeError: The Properties instance is not for an ``MSGFile``
338
+ object.
339
+ """
340
+ if self.__ac is None:
341
+ raise TypeError('Attachment properties do not contain an attachment count.')
342
+ return self.__ac
343
+
344
+ @attachmentCount.setter
345
+ def attachmentCount(self, value: int) -> None:
346
+ if not self.__writable:
347
+ raise NotWritableError('PropertiesStore object is not writable.')
348
+
349
+ if not isinstance(value, int):
350
+ raise TypeError(':property attachmentCount: must be an int.')
351
+
352
+ if self.__ac is None:
353
+ raise TypeError('Attachment properties do not contain an attachment count.')
354
+
355
+ self.__ac = value
356
+
357
+ @property
358
+ def date(self) -> Optional[datetime.datetime]:
359
+ """
360
+ Returns the send date contained in the Properties file.
361
+ """
362
+ try:
363
+ return self.__date
364
+ except AttributeError:
365
+ self.__date = None
366
+ if '00390040' in self:
367
+ dateValue = self.getValue('00390040')
368
+ # A date can be bytes if it fails to initialize, so we check it
369
+ # first.
370
+ if isinstance(dateValue, datetime.datetime):
371
+ self.__date = dateValue
372
+ return self.__date
373
+
374
+ @property
375
+ def isError(self) -> bool:
376
+ """
377
+ Whether the instance is in an invalid state.
378
+
379
+ If the instance is not writable and was given no data, this will be
380
+ ``True``.
381
+ """
382
+ return self.__isError
383
+
384
+ @property
385
+ def nextAttachmentId(self) -> int:
386
+ """
387
+ The ID to use for naming the next Attachment object storage if one is
388
+ created inside the .msg file.
389
+
390
+ :raises NotWritableError: The setter was used on an unwritable instance.
391
+ :raises TypeError: The Properties instance is not for an ``MSGFile``
392
+ object.
393
+ """
394
+ if self.__naid is None:
395
+ raise TypeError('Attachment properties do not contain a next attachment ID.')
396
+ return self.__naid
397
+
398
+ @nextAttachmentId.setter
399
+ def nextAttachmentId(self, value: int) -> None:
400
+ if not self.__writable:
401
+ raise NotWritableError('PropertiesStore object is not writable.')
402
+
403
+ if not isinstance(value, int):
404
+ raise TypeError(':property nextAttachmentId: must be an int.')
405
+
406
+ if self.__ac is None:
407
+ raise TypeError('Attachment properties do not contain a next attachment ID.')
408
+
409
+ self.__naid = value
410
+
411
+ @property
412
+ def nextRecipientId(self) -> int:
413
+ """
414
+ The ID to use for naming the next Recipient object storage if one is
415
+ created inside the .msg file.
416
+
417
+ :raises NotWritableError: The setter was used on an unwritable instance.
418
+ :raises TypeError: The Properties instance is not for an ``MSGFile``
419
+ object.
420
+ """
421
+ if self.__nrid is None:
422
+ raise TypeError('Attachment properties do not contain a next recipient ID.')
423
+ return self.__nrid
424
+
425
+ @nextRecipientId.setter
426
+ def nextRecipientId(self, value: int) -> None:
427
+ if not self.__writable:
428
+ raise NotWritableError('PropertiesStore object is not writable.')
429
+
430
+ if not isinstance(value, int):
431
+ raise TypeError(':property nextRecipientId: must be an int.')
432
+
433
+ if self.__ac is None:
434
+ raise TypeError('Attachment properties do not contain a next recipient ID.')
435
+
436
+ self.__nrid = value
437
+
438
+ @property
439
+ def props(self) -> Dict[str, PropBase]:
440
+ """
441
+ Returns a copy of the internal properties dict.
442
+ """
443
+ return copy.deepcopy(self.__props)
444
+
445
+ @property
446
+ def recipientCount(self) -> int:
447
+ """
448
+ The number of Recipient objects for the ``MSGFile`` object.
449
+
450
+ :raises NotWritableError: The setter was used on an unwritable instance.
451
+ :raises TypeError: The Properties instance is not for an ``MSGFile``
452
+ object.
453
+ """
454
+ if self.__rc is None:
455
+ raise TypeError('Attachment properties do not contain a recipient count.')
456
+ return self.__rc
457
+
458
+ @recipientCount.setter
459
+ def recipientCount(self, value: int) -> None:
460
+ if not self.__writable:
461
+ raise NotWritableError('PropertiesStore object is not writable.')
462
+
463
+ if not isinstance(value, int):
464
+ raise TypeError(':property recipientCount: must be an int.')
465
+
466
+ if self.__ac is None:
467
+ raise TypeError('Attachment properties do not contain a recipient count.')
468
+
469
+ self.__nrid = value
470
+
471
+ @property
472
+ def writable(self) -> bool:
473
+ """
474
+ Whether the instance accepts modification.
475
+ """
476
+ return self.__writable
extract_msg/py.typed ADDED
File without changes