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,134 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'EmbeddedMsgAttachment',
6
+ ]
7
+
8
+
9
+ import os
10
+ import pathlib
11
+ import zipfile
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from .. import constants
16
+ from .attachment_base import AttachmentBase
17
+ from ..enums import AttachmentType, SaveType
18
+ from ..open_msg import openMsg
19
+ from ..utils import createZipOpen, prepareFilename
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from ..msg_classes import MSGFile
24
+ from ..properties import PropertiesStore
25
+
26
+
27
+ _saveDoc = AttachmentBase.save.__doc__
28
+
29
+
30
+ class EmbeddedMsgAttachment(AttachmentBase):
31
+ """
32
+ The attachment entry for an Embedded MSG file.
33
+ """
34
+
35
+ def __init__(self, msg: MSGFile, dir_: str, propStore: PropertiesStore):
36
+ super().__init__(msg, dir_, propStore)
37
+ self.__prefix = msg.prefixList + [dir_, '__substg1.0_3701000D']
38
+ self.__data = openMsg(self.msg.path, prefix = self.__prefix, parentMsg = self.msg, treePath = self.treePath, **self.msg.kwargs)
39
+
40
+ def getFilename(self, **kwargs) -> str:
41
+ """
42
+ Returns the filename to use for the attachment.
43
+
44
+ :param contentId: Use the contentId, if available.
45
+ :param customFilename: A custom name to use for the file.
46
+
47
+ If the filename starts with "UnknownFilename" then there is no guarantee
48
+ that the files will have exactly the same filename.
49
+ """
50
+ customFilename = kwargs.get('customFilename')
51
+ if customFilename:
52
+ customFilename = str(customFilename)
53
+ # First we need to validate it. If there are invalid characters,
54
+ # this will detect it.
55
+ if constants.re.INVALID_FILENAME_CHARS.search(customFilename):
56
+ raise ValueError('Invalid character found in customFilename. Must not contain any of the following characters: \\/:*?"<>|')
57
+ return customFilename
58
+ else:
59
+ return self.name
60
+
61
+ def save(self, **kwargs) -> constants.SAVE_TYPE:
62
+ # First check if we are skipping embedded messages and stop
63
+ # *immediately* if we are.
64
+ if kwargs.get('skipEmbedded'):
65
+ return (SaveType.NONE, None)
66
+
67
+ # We only need to handle things if we are saving as bytes.
68
+ if kwargs.get('extractEmbedded', False):
69
+ # Get the filename to use.
70
+ filename = self.getFilename(**kwargs)
71
+
72
+ # Someone managed to have a null character here, so let's get rid of
73
+ # that
74
+ filename = prepareFilename(filename)
75
+
76
+ # Get the maximum name length.
77
+ maxNameLength = kwargs.get('maxNameLength', 256)
78
+
79
+ # Make sure the filename is not longer than it should be.
80
+ if len(filename) > maxNameLength:
81
+ name, ext = os.path.splitext(filename)
82
+ filename = name[:maxNameLength - len(ext)] + ext
83
+
84
+ # Check if we are doing a zip file.
85
+ _zip = kwargs.get('zip')
86
+
87
+ createdZip = False
88
+ try:
89
+ # ZipFile handling.
90
+ if _zip:
91
+ # If we are doing a zip file, first check that we have been given a path.
92
+ if isinstance(_zip, (str, pathlib.Path)):
93
+ # If we have a path then we use the zip file.
94
+ _zip = zipfile.ZipFile(_zip, 'a', zipfile.ZIP_DEFLATED)
95
+ kwargs['zip'] = _zip
96
+ createdZip = True
97
+ # Path needs to be done in a special way if we are in a zip file.
98
+ customPath = pathlib.Path(kwargs.get('customPath', ''))
99
+ # Set the open command to be that of the zip file.
100
+ _open = createZipOpen(_zip.open)
101
+ # Zip files use w for writing in binary.
102
+ mode = 'w'
103
+ else:
104
+ customPath = pathlib.Path(kwargs.get('customPath', '.')).absolute()
105
+ mode = 'wb'
106
+ _open = open
107
+
108
+ fullFilename = self._handleFnc(_zip, filename, customPath, kwargs)
109
+
110
+ with _open(str(fullFilename), mode) as f:
111
+ self.data.export(f)
112
+
113
+ return (SaveType.FILE, str(fullFilename))
114
+ finally:
115
+ # Close the ZipFile if this function created it.
116
+ if _zip and createdZip:
117
+ _zip.close()
118
+ else:
119
+ # If we are letting the MSG file create stuff, just let it handle
120
+ # everything.
121
+ return self.data.save(**kwargs)
122
+
123
+ save.__doc__ = _saveDoc
124
+
125
+ @property
126
+ def data(self) -> MSGFile:
127
+ """
128
+ Returns the attachment data.
129
+ """
130
+ return self.__data
131
+
132
+ @property
133
+ def type(self) -> AttachmentType:
134
+ return AttachmentType.MSG
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'SignedAttachment',
6
+ ]
7
+
8
+
9
+ import email.message
10
+ import logging
11
+ import os
12
+ import pathlib
13
+ import weakref
14
+ import zipfile
15
+
16
+ from typing import List, Optional, Type, TYPE_CHECKING, Union
17
+
18
+ from .. import constants
19
+ from ..enums import AttachmentType, SaveType
20
+ from ..open_msg import openMsg
21
+ from ..utils import createZipOpen, inputToString, makeWeakRef, prepareFilename
22
+
23
+
24
+ # Allow for nice type checking.
25
+ if TYPE_CHECKING:
26
+ from ..msg_classes.msg import MSGFile
27
+
28
+ logger = logging.getLogger(__name__)
29
+ logger.addHandler(logging.NullHandler())
30
+
31
+
32
+ class SignedAttachment:
33
+ def __init__(self, msg, data: bytes, name: str, mimetype: str, node: email.message.Message):
34
+ """
35
+ :param msg: The MSGFile instance this attachment is associated with.
36
+ :param data: The bytes that compose this attachment.
37
+ :param name: The reported name of the attachment.
38
+ :param mimetype: The reported mimetype of the attachment.
39
+ :param node: The email Message instance for this node.
40
+ """
41
+ self.__asBytes = data
42
+ self.__name = name
43
+ self.__mimetype = mimetype
44
+ self.__msg = makeWeakRef(msg)
45
+ self.__node = node
46
+ self.__treePath = msg.treePath + [makeWeakRef(self)]
47
+
48
+ self.__data = None
49
+ # To add support for embedded MSG files, we are going to completely
50
+ # ignore the mimetype and just do a few simple checks to see if we can
51
+ # use the bytes as am embedded file.
52
+ if data[:8] == b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1':
53
+ try:
54
+ # While we have to pass a lot of data down to the file, we don't
55
+ # pass the prefix and parent MSG data, as it is not an *actual*
56
+ # embedded MSG file. We are just pretending that it is for the
57
+ # external API.
58
+ self.__data = openMsg(data, treePath = self.__treePath, **msg.kwargs)
59
+ except Exception:
60
+ logger.exception('Signed message was an OLE file, but could not be read as an MSG file due to an exception.')
61
+
62
+ if self.__data is None:
63
+ self.__data = data
64
+
65
+ def save(self, **kwargs) -> constants.SAVE_TYPE:
66
+ """
67
+ Saves the attachment data.
68
+
69
+ The name of the file is determined by several factors. The first
70
+ thing that is checked is if you have provided :param customFilename:
71
+ to this function. If you have, that is the name that will be used.
72
+ Otherwise, the name from :attr:`name` will be used. After the name to
73
+ use has been determined, it will then be shortened to make sure that it
74
+ is not more than the value of :param maxNameLength:.
75
+
76
+ To change the directory that the attachment is saved to, set the value
77
+ of :param customPath: when calling this function. The default save
78
+ directory is the working directory.
79
+
80
+ If you want to save the contents into a ``ZipFile`` or similar object,
81
+ either pass a path to where you want to create one or pass an instance
82
+ to :param zip:. If :param zip: is an instance, :param customPath: will
83
+ refer to a location inside the zip file.
84
+ """
85
+ # First check if we are skipping embedded messages and stop
86
+ # *immediately* if we are.
87
+ if self.type is AttachmentType.SIGNED_EMBEDDED and kwargs.get('skipEmbedded'):
88
+ return (SaveType.NONE, None)
89
+
90
+ # If we are running the save function for the MSG file, just let it
91
+ # handle everything.
92
+ if (self.type is AttachmentType.SIGNED_EMBEDDED and
93
+ not kwargs.get('extractEmbedded', False)):
94
+ return self.saveEmbededMessage(**kwargs)
95
+
96
+ # Check if the user has specified a custom filename
97
+ filename = self.name
98
+
99
+ # Someone managed to have a null character here, so let's get rid of that
100
+ filename = prepareFilename(inputToString(filename, self.msg.stringEncoding))
101
+
102
+ # Get the maximum name length.
103
+ maxNameLength = kwargs.get('maxNameLength', 256)
104
+
105
+ # Make sure the filename is not longer than it should be.
106
+ if len(filename) > maxNameLength:
107
+ name, ext = os.path.splitext(filename)
108
+ filename = name[:maxNameLength - len(ext)] + ext
109
+
110
+ # Check if we are doing a zip file.
111
+ _zip = kwargs.get('zip')
112
+
113
+ createdZip = True
114
+ try:
115
+ # ZipFile handling.
116
+ if _zip:
117
+ # If we are doing a zip file, first check that we have been
118
+ # given a path.
119
+ if isinstance(_zip, (str, pathlib.Path)):
120
+ # If we have a path then we use the zip file.
121
+ _zip = zipfile.ZipFile(_zip, 'a', zipfile.ZIP_DEFLATED)
122
+ kwargs['zip'] = _zip
123
+ createdZip = True
124
+ else:
125
+ createdZip = False
126
+ # Path needs to be done in a special way if we are in a zip
127
+ # file.
128
+ customPath = pathlib.Path(kwargs.get('customPath', ''))
129
+ # Set the open command to be that of the zip file.
130
+ _open = createZipOpen(_zip.open)
131
+ # Zip files use w for writing in binary.
132
+ mode = 'w'
133
+ else:
134
+ customPath = pathlib.Path(kwargs.get('customPath', '.')).absolute()
135
+ mode = 'wb'
136
+ _open = open
137
+
138
+ fullFilename = self._handleFnc(_zip, filename, customPath, kwargs)
139
+
140
+ if self.type is AttachmentType.DATA:
141
+ with _open(str(fullFilename), mode) as f:
142
+ f.write(self.__data)
143
+
144
+ return (SaveType.FILE, str(fullFilename))
145
+ else:
146
+ with _open(str(fullFilename), mode) as f:
147
+ # We just use the data we were given for this one.
148
+ f.write(self.__asBytes)
149
+ return (SaveType.FILE, str(fullFilename))
150
+ finally:
151
+ # Close the ZipFile if this function created it.
152
+ if _zip and createdZip:
153
+ _zip.close()
154
+
155
+ def saveEmbededMessage(self, **kwargs) -> constants.SAVE_TYPE:
156
+ """
157
+ Seperate function from save to allow it to easily be overridden by a
158
+ subclass.
159
+ """
160
+ return self.data.save(**kwargs)
161
+
162
+ @property
163
+ def asBytes(self) -> bytes:
164
+ return self.__asBytes
165
+
166
+ @property
167
+ def data(self) -> Union[bytes, MSGFile]:
168
+ """
169
+ The bytes that compose this attachment.
170
+ """
171
+ return self.__data
172
+
173
+ @property
174
+ def dataType(self) -> Optional[Type[type]]:
175
+ """
176
+ The class that the data type will use, if it can be retrieved.
177
+
178
+ This is a safe way to do type checking on data before knowing if it will
179
+ raise an exception. Returns ``None`` if no data will be returns or if an
180
+ exception will be raised.
181
+ """
182
+ try:
183
+ return None if self.data is None else self.data.__class__
184
+ except Exception:
185
+ # All exceptions that accessing data would cause should be silenced.
186
+ return None
187
+
188
+ @property
189
+ def emailMessage(self) -> email.message.Message:
190
+ """
191
+ The email Message instance that is the source for this attachment.
192
+ """
193
+ return self.__node
194
+
195
+ @property
196
+ def mimetype(self) -> str:
197
+ """
198
+ The reported mimetype of the attachment.
199
+ """
200
+ return self.__mimetype
201
+
202
+ @property
203
+ def msg(self) -> MSGFile:
204
+ """
205
+ The ``MSGFile`` instance this attachment belongs to.
206
+
207
+ :raises ReferenceError: The associated ``MSGFile`` instance has been
208
+ garbage collected.
209
+ """
210
+ if (msg := self.__msg()) is None:
211
+ raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
212
+ return msg
213
+
214
+ @property
215
+ def name(self) -> str:
216
+ """
217
+ The reported name of this attachment.
218
+ """
219
+ return self.__name
220
+
221
+ longFilename = name
222
+ shortFilename = name
223
+
224
+ @property
225
+ def treePath(self) -> List[weakref.ReferenceType]:
226
+ """
227
+ A path, as a tuple of instances, needed to get to this instance through
228
+ the MSGFile-Attachment tree.
229
+ """
230
+ return self.__treePath
231
+
232
+ @property
233
+ def type(self) -> AttachmentType:
234
+ return AttachmentType.SIGNED if isinstance(self.__data, bytes) else AttachmentType.SIGNED_EMBEDDED
@@ -0,0 +1,40 @@
1
+ __all__ = [
2
+ 'UnsupportedAttachment',
3
+ ]
4
+
5
+
6
+ from .. import constants
7
+ from .attachment_base import AttachmentBase
8
+ from ..enums import AttachmentType, SaveType
9
+
10
+
11
+ class UnsupportedAttachment(AttachmentBase):
12
+ """
13
+ An attachment whose type is not currently supported.
14
+ """
15
+
16
+ def getFilename(self, **_) -> str:
17
+ raise NotImplementedError('Unsupported attachments cannot be saved.')
18
+
19
+ def save(self, **kwargs) -> constants.SAVE_TYPE:
20
+ """
21
+ Raises a NotImplementedError unless :param skipNotImplemented: is set to
22
+ ``True``.
23
+
24
+ If it is, returns a value that indicates no data was saved.
25
+ """
26
+ if kwargs.get('skipNotImplemented', False):
27
+ return (SaveType.NONE, None)
28
+
29
+ raise NotImplementedError('Unsupported attachments cannot be saved.')
30
+
31
+ @property
32
+ def data(self) -> None:
33
+ """
34
+ Broken attachments have no data.
35
+ """
36
+ return None
37
+
38
+ @property
39
+ def type(self) -> AttachmentType:
40
+ return AttachmentType.UNSUPPORTED
@@ -0,0 +1,74 @@
1
+ __all__ = [
2
+ 'WebAttachment',
3
+ ]
4
+
5
+
6
+ import functools
7
+
8
+ from typing import Optional
9
+
10
+ from .. import constants
11
+ from .attachment_base import AttachmentBase
12
+ from ..enums import AttachmentPermissionType, AttachmentType, SaveType
13
+
14
+
15
+ class WebAttachment(AttachmentBase):
16
+ """
17
+ An attachment that exists on the internet and not attached to the MSG file
18
+ directly.
19
+ """
20
+
21
+ def getFilename(self) -> str:
22
+ raise NotImplementedError('Cannot get the filename of a web attachment.')
23
+
24
+ def save(self, **kwargs) -> constants.SAVE_TYPE:
25
+ """
26
+ Raises a NotImplementedError unless :param skipNotImplemented: is set to
27
+ True.
28
+
29
+ If it is, returns a value that indicates no data was saved.
30
+ """
31
+ if kwargs.get('skipNotImplemented', False):
32
+ return (SaveType.NONE, None)
33
+
34
+ raise NotImplementedError('Web attachments cannot be saved.')
35
+
36
+ @property
37
+ def data(self) -> None:
38
+ """
39
+ The bytes making up the attachment data.
40
+ """
41
+ raise NotImplementedError('Cannot get the data of a web attachment.')
42
+
43
+ @functools.cached_property
44
+ def originalPermissionType(self) -> Optional[AttachmentPermissionType]:
45
+ """
46
+ The permission type data associated with a web reference attachment.
47
+ """
48
+ return self.getNamedAs('AttachmentOriginalPermissionType', constants.ps.PSETID_ATTACHMENT, AttachmentPermissionType)
49
+
50
+ @functools.cached_property
51
+ def permissionType(self) -> Optional[AttachmentPermissionType]:
52
+ """
53
+ The permission type data associated with a web reference attachment.
54
+ """
55
+ return self.getNamedAs('AttachmentPermissionType', constants.ps.PSETID_ATTACHMENT, AttachmentPermissionType)
56
+
57
+ @functools.cached_property
58
+ def providerName(self) -> Optional[str]:
59
+ """
60
+ The type of web service manipulating the attachment.
61
+ """
62
+ return self.getNamedProp('AttachmentProviderType', constants.ps.PSETID_ATTACHMENT)
63
+
64
+ @property
65
+ def type(self) -> AttachmentType:
66
+ return AttachmentType.WEB
67
+
68
+ @property
69
+ def url(self) -> Optional[str]:
70
+ """
71
+ The url for the web attachment. If this is not set, that is probably an
72
+ error.
73
+ """
74
+ return self.longPathname