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.
- extract_msg/__init__.py +71 -0
- extract_msg/__main__.py +115 -0
- extract_msg/_rtf/__init__.py +21 -0
- extract_msg/_rtf/create_doc.py +29 -0
- extract_msg/_rtf/inject_rtf.py +174 -0
- extract_msg/_rtf/token.py +35 -0
- extract_msg/_rtf/tokenize_rtf.py +253 -0
- extract_msg/attachments/__init__.py +144 -0
- extract_msg/attachments/attachment.py +188 -0
- extract_msg/attachments/attachment_base.py +730 -0
- extract_msg/attachments/broken_att.py +42 -0
- extract_msg/attachments/custom_att.py +173 -0
- extract_msg/attachments/custom_att_handler/__init__.py +80 -0
- extract_msg/attachments/custom_att_handler/custom_handler.py +134 -0
- extract_msg/attachments/custom_att_handler/lnk_obj_att.py +109 -0
- extract_msg/attachments/custom_att_handler/outlook_image_dib.py +141 -0
- extract_msg/attachments/emb_msg_att.py +134 -0
- extract_msg/attachments/signed_att.py +234 -0
- extract_msg/attachments/unsupported_att.py +40 -0
- extract_msg/attachments/web_att.py +74 -0
- extract_msg/constants/__init__.py +294 -0
- extract_msg/constants/ps.py +46 -0
- extract_msg/constants/re.py +42 -0
- extract_msg/constants/st.py +110 -0
- extract_msg/data/logging-config/logging-nt.json +64 -0
- extract_msg/data/logging-config/logging-posix.json +64 -0
- extract_msg/encoding/__init__.py +277 -0
- extract_msg/encoding/_dt/__init__.py +4 -0
- extract_msg/encoding/_dt/_mac_ce.py +8 -0
- extract_msg/encoding/_dt/_mac_cyrillic.py +8 -0
- extract_msg/encoding/_dt/_mac_greek.py +8 -0
- extract_msg/encoding/_dt/_mac_iceland.py +8 -0
- extract_msg/encoding/_dt/_mac_turkish.py +8 -0
- extract_msg/encoding/_dt/_win874_dec.py +8 -0
- extract_msg/encoding/_dt/_win950_dec.py +6 -0
- extract_msg/encoding/utils.py +329 -0
- extract_msg/enums.py +2067 -0
- extract_msg/exceptions.py +170 -0
- extract_msg/msg_classes/__init__.py +49 -0
- extract_msg/msg_classes/appointment.py +195 -0
- extract_msg/msg_classes/calendar.py +92 -0
- extract_msg/msg_classes/calendar_base.py +496 -0
- extract_msg/msg_classes/contact.py +1335 -0
- extract_msg/msg_classes/journal.py +131 -0
- extract_msg/msg_classes/meeting_cancellation.py +108 -0
- extract_msg/msg_classes/meeting_exception.py +55 -0
- extract_msg/msg_classes/meeting_forward.py +115 -0
- extract_msg/msg_classes/meeting_related.py +65 -0
- extract_msg/msg_classes/meeting_request.py +189 -0
- extract_msg/msg_classes/meeting_response.py +74 -0
- extract_msg/msg_classes/message.py +12 -0
- extract_msg/msg_classes/message_base.py +1382 -0
- extract_msg/msg_classes/message_signed.py +17 -0
- extract_msg/msg_classes/message_signed_base.py +168 -0
- extract_msg/msg_classes/msg.py +1172 -0
- extract_msg/msg_classes/post.py +53 -0
- extract_msg/msg_classes/sticky_note.py +67 -0
- extract_msg/msg_classes/task.py +363 -0
- extract_msg/msg_classes/task_request.py +86 -0
- extract_msg/null_date.py +34 -0
- extract_msg/ole_writer.py +1086 -0
- extract_msg/open_msg.py +204 -0
- extract_msg/properties/__init__.py +23 -0
- extract_msg/properties/named.py +481 -0
- extract_msg/properties/prop.py +506 -0
- extract_msg/properties/properties_store.py +476 -0
- extract_msg/py.typed +0 -0
- extract_msg/recipient.py +362 -0
- extract_msg/structures/__init__.py +34 -0
- extract_msg/structures/_helpers.py +345 -0
- extract_msg/structures/business_card.py +422 -0
- extract_msg/structures/cfoas.py +108 -0
- extract_msg/structures/contact_link_entry.py +51 -0
- extract_msg/structures/dev_mode_a.py +621 -0
- extract_msg/structures/dv_target_device.py +168 -0
- extract_msg/structures/entry_id.py +697 -0
- extract_msg/structures/misc_id.py +222 -0
- extract_msg/structures/mon_stream.py +55 -0
- extract_msg/structures/odt.py +74 -0
- extract_msg/structures/ole_pres.py +302 -0
- extract_msg/structures/ole_stream_struct.py +117 -0
- extract_msg/structures/recurrence_pattern.py +199 -0
- extract_msg/structures/report_tag.py +127 -0
- extract_msg/structures/system_time.py +158 -0
- extract_msg/structures/time_zone_definition.py +111 -0
- extract_msg/structures/time_zone_struct.py +102 -0
- extract_msg/structures/toc_entry.py +137 -0
- extract_msg/structures/tz_rule.py +148 -0
- extract_msg/utils.py +1270 -0
- extract_msg-0.48.2.dist-info/LICENSE.txt +674 -0
- extract_msg-0.48.2.dist-info/METADATA +321 -0
- extract_msg-0.48.2.dist-info/RECORD +95 -0
- extract_msg-0.48.2.dist-info/WHEEL +5 -0
- extract_msg-0.48.2.dist-info/entry_points.txt +2 -0
- extract_msg-0.48.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Submodule for attachment classes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
# Modules.
|
|
10
|
+
'custom_att_handler',
|
|
11
|
+
|
|
12
|
+
# Classes.
|
|
13
|
+
'Attachment',
|
|
14
|
+
'AttachmentBase',
|
|
15
|
+
'BrokenAttachment',
|
|
16
|
+
'CustomAttachmentHandler',
|
|
17
|
+
'EmbeddedMsgAttachment',
|
|
18
|
+
'SignedAttachment',
|
|
19
|
+
'UnsupportedAttachment',
|
|
20
|
+
'WebAttachment',
|
|
21
|
+
|
|
22
|
+
# Functions.
|
|
23
|
+
'initStandardAttachment',
|
|
24
|
+
'registerHandler',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from . import custom_att_handler
|
|
29
|
+
from .attachment import Attachment
|
|
30
|
+
from .attachment_base import AttachmentBase
|
|
31
|
+
from .broken_att import BrokenAttachment
|
|
32
|
+
from .custom_att import CustomAttachment
|
|
33
|
+
from .custom_att_handler import CustomAttachmentHandler, registerHandler
|
|
34
|
+
from .emb_msg_att import EmbeddedMsgAttachment
|
|
35
|
+
from .signed_att import SignedAttachment
|
|
36
|
+
from .unsupported_att import UnsupportedAttachment
|
|
37
|
+
from .web_att import WebAttachment
|
|
38
|
+
|
|
39
|
+
import logging as _logging
|
|
40
|
+
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from ..msg_classes import MSGFile
|
|
46
|
+
|
|
47
|
+
_logger = _logging.getLogger(__name__)
|
|
48
|
+
_logger.addHandler(_logging.NullHandler())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def initStandardAttachment(msg: MSGFile, dir_: str) -> AttachmentBase:
|
|
52
|
+
"""
|
|
53
|
+
Returns an instance of AttachmentBase for the attachment in the MSG file at
|
|
54
|
+
the specified internal directory.
|
|
55
|
+
|
|
56
|
+
:param errorBehavior: Used to tell the function what to do on errors.
|
|
57
|
+
"""
|
|
58
|
+
from ..properties import PropertiesStore
|
|
59
|
+
from ..enums import ErrorBehavior, PropertiesType
|
|
60
|
+
from ..exceptions import (
|
|
61
|
+
FeatureNotImplemented, StandardViolationError,
|
|
62
|
+
UnrecognizedMSGTypeError
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# First, create the properties store to check things like attachment type.
|
|
66
|
+
propertiesStream = msg.getStream([dir_, '__properties_version1.0'])
|
|
67
|
+
propStore = PropertiesStore(propertiesStream, PropertiesType.ATTACHMENT)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Now that we have the properties store, attempt to check what type it
|
|
71
|
+
# is.
|
|
72
|
+
if '37050003' not in propStore:
|
|
73
|
+
from ..properties.prop import createProp
|
|
74
|
+
|
|
75
|
+
_logger.warning(f'Attachment method property not found on attachment {dir_}. Code will attempt to guess the type.')
|
|
76
|
+
_logger.log(5, propStore)
|
|
77
|
+
|
|
78
|
+
# Because this condition is actually kind of a violation of the
|
|
79
|
+
# standard, we are just going to do this in a dumb way.
|
|
80
|
+
# Basically we are going to try to set the attach method
|
|
81
|
+
# *manually* just so I don't have to go and modify the
|
|
82
|
+
# following code.
|
|
83
|
+
if msg.exists([dir_, '__substg1.0_37010102']):
|
|
84
|
+
# Set it as data and call it a day.
|
|
85
|
+
propData = b'\x03\x00\x057\x07\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
|
|
86
|
+
elif msg.exists([dir_, '__substg1.0_3701000D']):
|
|
87
|
+
# If it is a folder and we have properties, call it an MSG
|
|
88
|
+
# file.
|
|
89
|
+
if msg.exists([dir_, '__substg1.0_3701000D/__properties_version1.0']):
|
|
90
|
+
propData = b'\x03\x00\x057\x07\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00'
|
|
91
|
+
else:
|
|
92
|
+
# Call if custom attachment data.
|
|
93
|
+
propData = b'\x03\x00\x057\x07\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00'
|
|
94
|
+
else:
|
|
95
|
+
# Can't autodetect it, so throw an error.
|
|
96
|
+
raise StandardViolationError(f'Attachment method missing on attachment {dir_}, and it could not be determined automatically.')
|
|
97
|
+
|
|
98
|
+
propStore.addProperty(createProp(propData), True)
|
|
99
|
+
|
|
100
|
+
attMethod = propStore.getValue('37050003', 0) & 7
|
|
101
|
+
|
|
102
|
+
if msg.exists([dir_, '__substg1.0_37010102']):
|
|
103
|
+
return Attachment(msg, dir_, propStore)
|
|
104
|
+
if msg.exists([dir_, '__substg1.0_3701000D']):
|
|
105
|
+
if attMethod != 5:
|
|
106
|
+
return CustomAttachment(msg, dir_, propStore)
|
|
107
|
+
else:
|
|
108
|
+
return EmbeddedMsgAttachment(msg, dir_, propStore)
|
|
109
|
+
|
|
110
|
+
if attMethod == 7:
|
|
111
|
+
return WebAttachment(msg, dir_, propStore)
|
|
112
|
+
|
|
113
|
+
# Error handling.
|
|
114
|
+
if attMethod == 1:
|
|
115
|
+
raise StandardViolationError('Attachments of type data MUST have a data stream.')
|
|
116
|
+
if attMethod == 0:
|
|
117
|
+
raise NotImplementedError('extract-msg does not support attachments of type afNone.')
|
|
118
|
+
if attMethod == 2:
|
|
119
|
+
raise NotImplementedError('extract-msg does not support attachments of type afByReference. Contact the developer for support.')
|
|
120
|
+
if attMethod == 4:
|
|
121
|
+
raise NotImplementedError('extract-msg does not support attachments of type afByReferenceOnly. Contact the developer for support.')
|
|
122
|
+
|
|
123
|
+
raise NotImplementedError(f'Could not determine attachment type ({attMethod})!')
|
|
124
|
+
|
|
125
|
+
except (FeatureNotImplemented,
|
|
126
|
+
NotImplementedError,
|
|
127
|
+
UnrecognizedMSGTypeError):
|
|
128
|
+
if ErrorBehavior.ATTACH_NOT_IMPLEMENTED in msg.errorBehavior:
|
|
129
|
+
_logger.exception(f'Error processing attachment at {dir_}')
|
|
130
|
+
return UnsupportedAttachment(msg, dir_, propStore)
|
|
131
|
+
else:
|
|
132
|
+
raise
|
|
133
|
+
except StandardViolationError:
|
|
134
|
+
if ErrorBehavior.STANDARDS_VIOLATION in msg.errorBehavior:
|
|
135
|
+
_logger.exception(f'Unresolvable standards violation in {dir_}')
|
|
136
|
+
return BrokenAttachment(msg, dir_, propStore)
|
|
137
|
+
else:
|
|
138
|
+
raise
|
|
139
|
+
except Exception:
|
|
140
|
+
if ErrorBehavior.ATTACH_BROKEN in msg.errorBehavior:
|
|
141
|
+
_logger.exception(f'Error processing attachment at {dir_}')
|
|
142
|
+
return BrokenAttachment(msg, dir_, propStore)
|
|
143
|
+
else:
|
|
144
|
+
raise
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'Attachment',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
import random
|
|
13
|
+
import string
|
|
14
|
+
import zipfile
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from .. import constants
|
|
19
|
+
from .attachment_base import AttachmentBase
|
|
20
|
+
from ..enums import AttachmentType, SaveType
|
|
21
|
+
from ..utils import createZipOpen, inputToString, prepareFilename
|
|
22
|
+
from ..properties import PropertiesStore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Allow for nice type checking.
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from ..msg_classes.msg import MSGFile
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
logger.addHandler(logging.NullHandler())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Attachment(AttachmentBase):
|
|
34
|
+
"""
|
|
35
|
+
A standard data attachment of an MSG file.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, msg: MSGFile, dir_: str, propStore: PropertiesStore):
|
|
39
|
+
super().__init__(msg, dir_, propStore)
|
|
40
|
+
self.__data = self.getStream('__substg1.0_37010102')
|
|
41
|
+
|
|
42
|
+
def getFilename(self, **kwargs) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Returns the filename to use for the attachment.
|
|
45
|
+
|
|
46
|
+
:param contentId: Use the contentId, if available.
|
|
47
|
+
:param customFilename: A custom name to use for the file.
|
|
48
|
+
|
|
49
|
+
If the filename starts with "UnknownFilename" then there is no guarantee
|
|
50
|
+
that the files will have exactly the same filename.
|
|
51
|
+
"""
|
|
52
|
+
filename = None
|
|
53
|
+
customFilename = kwargs.get('customFilename')
|
|
54
|
+
if customFilename:
|
|
55
|
+
customFilename = str(customFilename)
|
|
56
|
+
# First we need to validate it. If there are invalid characters,
|
|
57
|
+
# this will detect it.
|
|
58
|
+
if constants.re.INVALID_FILENAME_CHARS.search(customFilename):
|
|
59
|
+
raise ValueError('Invalid character found in customFilename. Must not contain any of the following characters: \\/:*?"<>|')
|
|
60
|
+
filename = customFilename
|
|
61
|
+
else:
|
|
62
|
+
# If not...
|
|
63
|
+
# Check if user wants to save the file under the Content-ID.
|
|
64
|
+
if kwargs.get('contentId', False):
|
|
65
|
+
filename = self.cid
|
|
66
|
+
# If we are here, try to get the filename however else we can.
|
|
67
|
+
if not filename:
|
|
68
|
+
filename = self.name
|
|
69
|
+
# Otherwise just make something up!
|
|
70
|
+
if not filename:
|
|
71
|
+
return self.randomFilename
|
|
72
|
+
|
|
73
|
+
return filename
|
|
74
|
+
|
|
75
|
+
def regenerateRandomName(self) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Used to regenerate the random filename used if the attachment cannot
|
|
78
|
+
find a usable filename.
|
|
79
|
+
"""
|
|
80
|
+
self.__randomName = inputToString('UnknownFilename ' + \
|
|
81
|
+
''.join(random.choice(string.ascii_uppercase + string.digits)
|
|
82
|
+
for _ in range(5)) + '.bin', 'ascii')
|
|
83
|
+
|
|
84
|
+
def save(self, **kwargs) -> constants.SAVE_TYPE:
|
|
85
|
+
"""
|
|
86
|
+
Saves the attachment data.
|
|
87
|
+
|
|
88
|
+
The name of the file is determined by several factors. The first
|
|
89
|
+
thing that is checked is if you have provided :param customFilename:
|
|
90
|
+
to this function. If you have, that is the name that will be used.
|
|
91
|
+
If no custom name has been provided and :param contentId: is ``True``,
|
|
92
|
+
the file will be saved using the content ID of the attachment. If
|
|
93
|
+
it is not found or :param contentId: is ``False``, the long filename
|
|
94
|
+
will be used. If the long filename is not found, the short one will
|
|
95
|
+
be used. If after all of this a usable filename has not been found, a
|
|
96
|
+
random one will be used (accessible from :meth:`randomFilename`).
|
|
97
|
+
After the name to use has been determined, it will then be shortened to
|
|
98
|
+
make sure that it is not more than the value of :param maxNameLength:.
|
|
99
|
+
|
|
100
|
+
To change the directory that the attachment is saved to, set the value
|
|
101
|
+
of :param customPath: when calling this function. The default save
|
|
102
|
+
directory is the working directory.
|
|
103
|
+
|
|
104
|
+
If you want to save the contents into a ``ZipFile`` or similar object,
|
|
105
|
+
either pass a path to where you want to create one or pass an instance
|
|
106
|
+
to :param zip:. If :param zip: is an instance, :param customPath: will
|
|
107
|
+
refer to a location inside the zip file.
|
|
108
|
+
|
|
109
|
+
:param extractEmbedded: If ``True``, causes the attachment, should it
|
|
110
|
+
be an embedded MSG file, to save as a .msg file instead of calling
|
|
111
|
+
it's save function.
|
|
112
|
+
:param skipEmbedded: If ``True``, skips saving this attachment if it is
|
|
113
|
+
an embedded MSG file.
|
|
114
|
+
"""
|
|
115
|
+
# Get the filename to use.
|
|
116
|
+
filename = self.getFilename(**kwargs)
|
|
117
|
+
|
|
118
|
+
# Someone managed to have a null character here, so let's get rid of
|
|
119
|
+
# that
|
|
120
|
+
filename = prepareFilename(inputToString(filename, self.msg.stringEncoding))
|
|
121
|
+
|
|
122
|
+
# Get the maximum name length.
|
|
123
|
+
maxNameLength = kwargs.get('maxNameLength', 256)
|
|
124
|
+
|
|
125
|
+
# Make sure the filename is not longer than it should be.
|
|
126
|
+
if len(filename) > maxNameLength:
|
|
127
|
+
name, ext = os.path.splitext(filename)
|
|
128
|
+
filename = name[:maxNameLength - len(ext)] + ext
|
|
129
|
+
|
|
130
|
+
# Check if we are doing a zip file.
|
|
131
|
+
_zip = kwargs.get('zip')
|
|
132
|
+
|
|
133
|
+
createdZip = False
|
|
134
|
+
try:
|
|
135
|
+
# ZipFile handling.
|
|
136
|
+
if _zip:
|
|
137
|
+
# If we are doing a zip file, first check that we have been
|
|
138
|
+
# given a path.
|
|
139
|
+
if isinstance(_zip, (str, pathlib.Path)):
|
|
140
|
+
# If we have a path then we use the zip file.
|
|
141
|
+
_zip = zipfile.ZipFile(_zip, 'a', zipfile.ZIP_DEFLATED)
|
|
142
|
+
kwargs['zip'] = _zip
|
|
143
|
+
createdZip = True
|
|
144
|
+
# Path needs to be done in a special way if we are in a zip
|
|
145
|
+
# file.
|
|
146
|
+
customPath = pathlib.Path(kwargs.get('customPath', ''))
|
|
147
|
+
# Set the open command to be that of the zip file.
|
|
148
|
+
_open = createZipOpen(_zip.open)
|
|
149
|
+
# Zip files use w for writing in binary.
|
|
150
|
+
mode = 'w'
|
|
151
|
+
else:
|
|
152
|
+
customPath = pathlib.Path(kwargs.get('customPath', '.')).absolute()
|
|
153
|
+
mode = 'wb'
|
|
154
|
+
_open = open
|
|
155
|
+
|
|
156
|
+
fullFilename = self._handleFnc(_zip, filename, customPath, kwargs)
|
|
157
|
+
|
|
158
|
+
with _open(str(fullFilename), mode) as f:
|
|
159
|
+
f.write(self.__data)
|
|
160
|
+
|
|
161
|
+
return (SaveType.FILE, str(fullFilename))
|
|
162
|
+
|
|
163
|
+
finally:
|
|
164
|
+
# Close the ZipFile if this function created it.
|
|
165
|
+
if _zip and createdZip:
|
|
166
|
+
_zip.close()
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def data(self) -> bytes:
|
|
170
|
+
"""
|
|
171
|
+
The bytes making up the attachment data.
|
|
172
|
+
"""
|
|
173
|
+
return self.__data
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def randomFilename(self) -> str:
|
|
177
|
+
"""
|
|
178
|
+
The random filename to be used by this attachment.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
return self.__randomName
|
|
182
|
+
except AttributeError:
|
|
183
|
+
self.regenerateRandomName()
|
|
184
|
+
return self.__randomName
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def type(self) -> AttachmentType:
|
|
188
|
+
return AttachmentType.DATA
|