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,42 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'BrokenAttachment',
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .. import constants
|
|
7
|
+
from .attachment_base import AttachmentBase
|
|
8
|
+
from ..enums import AttachmentType, SaveType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BrokenAttachment(AttachmentBase):
|
|
12
|
+
"""
|
|
13
|
+
An attachment that has suffered a fatal error.
|
|
14
|
+
|
|
15
|
+
Will not generate from a NotImplementedError exception.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def getFilename(self, **_) -> str:
|
|
19
|
+
raise NotImplementedError('Broken attachments cannot be saved.')
|
|
20
|
+
|
|
21
|
+
def save(self, **kwargs) -> constants.SAVE_TYPE:
|
|
22
|
+
"""
|
|
23
|
+
Raises a NotImplementedError unless :param skipNotImplemented: is set to
|
|
24
|
+
True.
|
|
25
|
+
|
|
26
|
+
If it is, returns a value that indicates no data was saved.
|
|
27
|
+
"""
|
|
28
|
+
if kwargs.get('skipNotImplemented', False):
|
|
29
|
+
return (SaveType.NONE, None)
|
|
30
|
+
|
|
31
|
+
raise NotImplementedError('Broken attachments cannot be saved.')
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def data(self) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Broken attachments have no data.
|
|
37
|
+
"""
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def type(self) -> AttachmentType:
|
|
42
|
+
return AttachmentType.BROKEN
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'CustomAttachment',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import random
|
|
12
|
+
import string
|
|
13
|
+
import zipfile
|
|
14
|
+
|
|
15
|
+
from typing import Optional, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from .. import constants
|
|
18
|
+
from .attachment_base import AttachmentBase
|
|
19
|
+
from .custom_att_handler import CustomAttachmentHandler, getHandler
|
|
20
|
+
from ..enums import AttachmentType, SaveType
|
|
21
|
+
from ..utils import createZipOpen, inputToString, prepareFilename
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ..msg_classes import MSGFile
|
|
26
|
+
from ..properties import PropertiesStore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_saveDoc = AttachmentBase.save.__doc__
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CustomAttachment(AttachmentBase):
|
|
33
|
+
"""
|
|
34
|
+
The attachment entry for custom attachments.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, msg: MSGFile, dir_: str, propStore: PropertiesStore):
|
|
38
|
+
super().__init__(msg, dir_, propStore)
|
|
39
|
+
|
|
40
|
+
self.__customHandler = getHandler(self)
|
|
41
|
+
self.__data = self.__customHandler.data
|
|
42
|
+
|
|
43
|
+
def getFilename(self, **kwargs) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Returns the filename to use for the attachment.
|
|
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
|
+
:param contentId: Use the contentId, if available.
|
|
51
|
+
:param customFilename: A custom name to use for the file.
|
|
52
|
+
"""
|
|
53
|
+
filename = None
|
|
54
|
+
customFilename = kwargs.get('customFilename')
|
|
55
|
+
if customFilename:
|
|
56
|
+
customFilename = str(customFilename)
|
|
57
|
+
# First we need to validate it. If there are invalid characters,
|
|
58
|
+
# this will detect it.
|
|
59
|
+
if constants.re.INVALID_FILENAME_CHARS.search(customFilename):
|
|
60
|
+
raise ValueError('Invalid character found in customFilename. Must not contain any of the following characters: \\/:*?"<>|')
|
|
61
|
+
filename = customFilename
|
|
62
|
+
else:
|
|
63
|
+
# If not...
|
|
64
|
+
# Check if user wants to save the file under the Content-ID.
|
|
65
|
+
if kwargs.get('contentId', False):
|
|
66
|
+
filename = self.cid
|
|
67
|
+
# Try to get the name from the custom handler.
|
|
68
|
+
filename = self.customHandler.name
|
|
69
|
+
# If we are here, try to get the filename however else we can.
|
|
70
|
+
if not filename:
|
|
71
|
+
filename = self.name
|
|
72
|
+
# Otherwise just make something up!
|
|
73
|
+
if not filename:
|
|
74
|
+
return self.randomFilename
|
|
75
|
+
|
|
76
|
+
return filename
|
|
77
|
+
|
|
78
|
+
def regenerateRandomName(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Used to regenerate the random filename used if the attachment cannot
|
|
81
|
+
find a usable filename.
|
|
82
|
+
"""
|
|
83
|
+
self.__randomName = 'UnknownFilename ' + \
|
|
84
|
+
''.join(random.choice(string.ascii_uppercase + string.digits)
|
|
85
|
+
for _ in range(5)) + '.bin'
|
|
86
|
+
|
|
87
|
+
def save(self, **kwargs) -> constants.SAVE_TYPE:
|
|
88
|
+
# Immediate check to see if there is anything to save.
|
|
89
|
+
if self.data is None:
|
|
90
|
+
return (SaveType.NONE, None)
|
|
91
|
+
|
|
92
|
+
# Get the filename to use.
|
|
93
|
+
filename = self.getFilename(**kwargs)
|
|
94
|
+
|
|
95
|
+
# Someone managed to have a null character here, so let's get rid of
|
|
96
|
+
# that
|
|
97
|
+
filename = prepareFilename(inputToString(filename, self.msg.stringEncoding))
|
|
98
|
+
|
|
99
|
+
# Get the maximum name length.
|
|
100
|
+
maxNameLength = kwargs.get('maxNameLength', 256)
|
|
101
|
+
|
|
102
|
+
# Make sure the filename is not longer than it should be.
|
|
103
|
+
if len(filename) > maxNameLength:
|
|
104
|
+
name, ext = os.path.splitext(filename)
|
|
105
|
+
filename = name[:maxNameLength - len(ext)] + ext
|
|
106
|
+
|
|
107
|
+
# Check if we are doing a zip file.
|
|
108
|
+
_zip = kwargs.get('zip')
|
|
109
|
+
|
|
110
|
+
createdZip = False
|
|
111
|
+
try:
|
|
112
|
+
# ZipFile handling.
|
|
113
|
+
if _zip:
|
|
114
|
+
# If we are doing a zip file, first check that we have been given a path.
|
|
115
|
+
if isinstance(_zip, (str, pathlib.Path)):
|
|
116
|
+
# If we have a path then we use the zip file.
|
|
117
|
+
_zip = zipfile.ZipFile(_zip, 'a', zipfile.ZIP_DEFLATED)
|
|
118
|
+
kwargs['zip'] = _zip
|
|
119
|
+
createdZip = True
|
|
120
|
+
# Path needs to be done in a special way if we are in a zip file.
|
|
121
|
+
customPath = pathlib.Path(kwargs.get('customPath', ''))
|
|
122
|
+
# Set the open command to be that of the zip file.
|
|
123
|
+
_open = createZipOpen(_zip.open)
|
|
124
|
+
# Zip files use w for writing in binary.
|
|
125
|
+
mode = 'w'
|
|
126
|
+
else:
|
|
127
|
+
customPath = pathlib.Path(kwargs.get('customPath', '.')).absolute()
|
|
128
|
+
mode = 'wb'
|
|
129
|
+
_open = open
|
|
130
|
+
|
|
131
|
+
fullFilename = self._handleFnc(_zip, filename, customPath, kwargs)
|
|
132
|
+
|
|
133
|
+
with _open(str(fullFilename), mode) as f:
|
|
134
|
+
f.write(self.__data)
|
|
135
|
+
|
|
136
|
+
return (SaveType.FILE, str(fullFilename))
|
|
137
|
+
finally:
|
|
138
|
+
# Close the ZipFile if this function created it.
|
|
139
|
+
if _zip and createdZip:
|
|
140
|
+
_zip.close()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def customHandler(self) -> Optional[CustomAttachmentHandler]:
|
|
145
|
+
"""
|
|
146
|
+
The instance of the custom handler associated with this attachment, if
|
|
147
|
+
it has one.
|
|
148
|
+
"""
|
|
149
|
+
return self.__customHandler
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def data(self) -> Optional[bytes]:
|
|
153
|
+
"""
|
|
154
|
+
The attachment data, if any.
|
|
155
|
+
|
|
156
|
+
Returns ``None`` if there is no data to save.
|
|
157
|
+
"""
|
|
158
|
+
return self.__data
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def randomFilename(self) -> str:
|
|
162
|
+
"""
|
|
163
|
+
The random filename to be used by this attachment.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
return self.__randomName
|
|
167
|
+
except AttributeError:
|
|
168
|
+
self.regenerateRandomName()
|
|
169
|
+
return self.__randomName
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def type(self) -> AttachmentType:
|
|
173
|
+
return AttachmentType.CUSTOM
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Submodule designed to help with saving and using custom attachments.
|
|
6
|
+
|
|
7
|
+
Custom attachments are those follow standards not defined in the MSG
|
|
8
|
+
documentation. Use the function `getHandler` to get an instance of a subclass of
|
|
9
|
+
CustomAttachmentHandler.
|
|
10
|
+
|
|
11
|
+
It should hopefully be completely unnecessary for your code to know what type of
|
|
12
|
+
handler it is using, as the abstract base class should give all of the functions
|
|
13
|
+
you would typically want.
|
|
14
|
+
|
|
15
|
+
If you would like to add your own handler, simply subclass
|
|
16
|
+
CustomAttachmentHandler and add it using the `registerHandler` function.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
# Classes.
|
|
21
|
+
'CustomAttachmentHandler',
|
|
22
|
+
'LinkedObjectAttachment',
|
|
23
|
+
'OutlookImageDIB',
|
|
24
|
+
|
|
25
|
+
# Functions.
|
|
26
|
+
'getHandler',
|
|
27
|
+
'registerHandler',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
from typing import List, Type, TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
from .custom_handler import CustomAttachmentHandler
|
|
34
|
+
from ...exceptions import FeatureNotImplemented
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Create a way to register handlers.
|
|
38
|
+
_knownHandlers: List[Type[CustomAttachmentHandler]] = []
|
|
39
|
+
|
|
40
|
+
def registerHandler(handler: Type[CustomAttachmentHandler]) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Registers the CustomAttachmentHandler subclass as a handler.
|
|
43
|
+
|
|
44
|
+
:raises TypeError: The handler was not a subclass of
|
|
45
|
+
CustomAttachmentHandler.
|
|
46
|
+
"""
|
|
47
|
+
# Make sure it is a subclass of CustomAttachmentHandler.
|
|
48
|
+
if not isinstance(handler, type):
|
|
49
|
+
raise ValueError(':param handler: must be a class, not an instance of a class.')
|
|
50
|
+
if not issubclass(handler, CustomAttachmentHandler): # pyright: ignore
|
|
51
|
+
raise ValueError(':param handler: must be a subclass of CustomAttachmentHandler.')
|
|
52
|
+
_knownHandlers.append(handler)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Import built-in handler modules. They will all automatically register their
|
|
56
|
+
# respecive handler(s).
|
|
57
|
+
from .outlook_image_dib import OutlookImageDIB
|
|
58
|
+
from .lnk_obj_att import LinkedObjectAttachment
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from ..attachment_base import AttachmentBase
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Function designed to route to the correct handler.
|
|
66
|
+
def getHandler(attachment: AttachmentBase) -> CustomAttachmentHandler:
|
|
67
|
+
"""
|
|
68
|
+
Takes an attachment and uses it to find the correct handler.
|
|
69
|
+
|
|
70
|
+
Returns an instance created using the specified attachment.
|
|
71
|
+
|
|
72
|
+
:raises NotImplementedError: No handler could be found.
|
|
73
|
+
:raises ValueError: A handler was found, but something was wrong with the
|
|
74
|
+
attachment data.
|
|
75
|
+
"""
|
|
76
|
+
for handler in _knownHandlers:
|
|
77
|
+
if handler.isCorrectHandler(attachment):
|
|
78
|
+
return handler(attachment)
|
|
79
|
+
|
|
80
|
+
raise FeatureNotImplemented(f'No valid handler could be found for the attachment. Contact the developers for help. If the CLSID is not all zeros, include it in the title or message. (CLSID: {attachment.clsid})')
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'CustomAttachmentHandler',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import abc
|
|
10
|
+
import functools
|
|
11
|
+
|
|
12
|
+
from typing import Dict, Optional, TYPE_CHECKING, TypeVar
|
|
13
|
+
|
|
14
|
+
from ...constants import MSG_PATH, OVERRIDE_CLASS
|
|
15
|
+
from ...structures.odt import ODTStruct
|
|
16
|
+
from ...structures.ole_pres import OLEPresentationStream
|
|
17
|
+
from ...structures.ole_stream_struct import OleStreamStruct
|
|
18
|
+
from ...utils import msgPathToString
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..attachment_base import AttachmentBase
|
|
23
|
+
|
|
24
|
+
_T = TypeVar('_T')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CustomAttachmentHandler(abc.ABC):
|
|
28
|
+
"""
|
|
29
|
+
A class designed to help with custom attachments that may require parsing in
|
|
30
|
+
special ways that are completely different from one another.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, attachment: AttachmentBase):
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.__att = attachment
|
|
36
|
+
|
|
37
|
+
def getStream(self, filename: MSG_PATH) -> Optional[bytes]:
|
|
38
|
+
"""
|
|
39
|
+
Gets a stream from the custom data directory.
|
|
40
|
+
"""
|
|
41
|
+
return self.attachment.getStream('__substg1.0_3701000D/' + msgPathToString(filename))
|
|
42
|
+
|
|
43
|
+
def getStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
|
|
44
|
+
"""
|
|
45
|
+
Returns the specified stream, modifying it to the specified class if it
|
|
46
|
+
is found.
|
|
47
|
+
|
|
48
|
+
:param overrideClass: Class/function to use to morph the data that was
|
|
49
|
+
read. The data will be the first argument to the class's __init__
|
|
50
|
+
function or the function itself, if that is what is provided. If
|
|
51
|
+
the value is None, this function is not called. If you want it to
|
|
52
|
+
be called regardless, you should handle the data directly.
|
|
53
|
+
"""
|
|
54
|
+
value = self.getStream(streamID)
|
|
55
|
+
|
|
56
|
+
if value is not None:
|
|
57
|
+
value = overrideClass(value)
|
|
58
|
+
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
@abc.abstractmethod
|
|
63
|
+
def isCorrectHandler(cls, attachment: AttachmentBase) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Checks if this is the correct handler for the attachment.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@abc.abstractmethod
|
|
69
|
+
def generateRtf(self) -> Optional[bytes]:
|
|
70
|
+
"""
|
|
71
|
+
Generates the RTF to inject in place of the \\objattph tag.
|
|
72
|
+
|
|
73
|
+
If this function should do nothing, returns ``None``.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def attachment(self) -> AttachmentBase:
|
|
78
|
+
"""
|
|
79
|
+
The attachment this handler is associated with.
|
|
80
|
+
"""
|
|
81
|
+
return self.__att
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
@abc.abstractmethod
|
|
85
|
+
def data(self) -> Optional[bytes]:
|
|
86
|
+
"""
|
|
87
|
+
Gets the data for the attachment.
|
|
88
|
+
|
|
89
|
+
If an attachment should do nothing when saving, returns ``None``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
@abc.abstractmethod
|
|
94
|
+
def name(self) -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Returns the name to be used when saving the attachment.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
@abc.abstractmethod
|
|
101
|
+
def obj(self) -> Optional[object]:
|
|
102
|
+
"""
|
|
103
|
+
Returns an object representing the data.
|
|
104
|
+
|
|
105
|
+
May return the same value as :property data:.
|
|
106
|
+
|
|
107
|
+
If there is no object to represent the custom attachment, including
|
|
108
|
+
``bytes``, returns ``None``.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@functools.cached_property
|
|
112
|
+
def objInfo(self) -> Optional[ODTStruct]:
|
|
113
|
+
"""
|
|
114
|
+
The structure representing the stream "\\x03ObjInfo", if it exists.
|
|
115
|
+
"""
|
|
116
|
+
return self.getStreamAs('\x03ObjInfo', ODTStruct)
|
|
117
|
+
|
|
118
|
+
@functools.cached_property
|
|
119
|
+
def ole(self) -> Optional[OleStreamStruct]:
|
|
120
|
+
"""
|
|
121
|
+
The structure representing the stream "\\x01Ole", if it exists.
|
|
122
|
+
"""
|
|
123
|
+
return self.getStreamAs('\x01Ole', OleStreamStruct)
|
|
124
|
+
|
|
125
|
+
@functools.cached_property
|
|
126
|
+
def presentationObjs(self) -> Optional[Dict[int, OLEPresentationStream]]:
|
|
127
|
+
"""
|
|
128
|
+
Returns a dict of all presentation streams, as bytes.
|
|
129
|
+
"""
|
|
130
|
+
return {
|
|
131
|
+
int(x[1][-3:]): self.getStreamAs(x[-1], OLEPresentationStream)
|
|
132
|
+
for x in self.attachment.listDir()
|
|
133
|
+
if x[0] == '__substg1.0_3701000D' and x[1].startswith('\x02OlePres')
|
|
134
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'LinkedObjectAttachment',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from . import registerHandler
|
|
13
|
+
from .custom_handler import CustomAttachmentHandler
|
|
14
|
+
from ...structures.entry_id import EntryID
|
|
15
|
+
from ...structures.ole_pres import OLEPresentationStream
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..attachment_base import AttachmentBase
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LinkedObjectAttachment(CustomAttachmentHandler):
|
|
23
|
+
"""
|
|
24
|
+
A link to an Outlook object.
|
|
25
|
+
|
|
26
|
+
Not *positive* I understand what this attachment type is, but this seems to
|
|
27
|
+
be the most likely name. Contains presentation data about how to render it
|
|
28
|
+
as well as properties with data that link to it. It looks *similar* to what
|
|
29
|
+
the documentation for Journal specifies would be it's custom attachment
|
|
30
|
+
type, however some small details don't perfectly add up.
|
|
31
|
+
|
|
32
|
+
I've also only seen this on Journal objects thus far.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, attachment: AttachmentBase):
|
|
36
|
+
super().__init__(attachment)
|
|
37
|
+
stream = attachment.getStream('__substg1.0_3701000D/\x03MailStream')
|
|
38
|
+
if not stream:
|
|
39
|
+
raise ValueError('MailStream could not be found.')
|
|
40
|
+
if len(stream) != 12:
|
|
41
|
+
raise ValueError('MailStream is the wrong length.')
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def isCorrectHandler(cls, attachment: AttachmentBase) -> bool:
|
|
45
|
+
if attachment.clsid != '00020D09-0000-0000-C000-000000000046':
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
def generateRtf(self) -> Optional[bytes]:
|
|
51
|
+
# TODO
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def data(self) -> None:
|
|
56
|
+
# This type of attachment has no direct associated data.
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def mailMsgAttFld(self) -> Optional[EntryID]:
|
|
61
|
+
"""
|
|
62
|
+
The EntryID of the folder of the linked Message object.
|
|
63
|
+
"""
|
|
64
|
+
return EntryID.autoCreate(self.getStream('MailMsgAttFld'))
|
|
65
|
+
|
|
66
|
+
@cached_property
|
|
67
|
+
def mailMsgAttMdb(self) -> Optional[EntryID]:
|
|
68
|
+
"""
|
|
69
|
+
The EntryID of the store of the linked Message object.
|
|
70
|
+
"""
|
|
71
|
+
return EntryID.autoCreate(self.getStream('MailMsgAttMdb'))
|
|
72
|
+
|
|
73
|
+
@cached_property
|
|
74
|
+
def mailMsgAttMsg(self) -> Optional[EntryID]:
|
|
75
|
+
"""
|
|
76
|
+
The EntryID linked Message object, required only if the
|
|
77
|
+
mailMsgAttSrchKey property is None.
|
|
78
|
+
"""
|
|
79
|
+
return EntryID.autoCreate(self.getStream('MailMsgAttMsg'))
|
|
80
|
+
|
|
81
|
+
@cached_property
|
|
82
|
+
def mailMsgAttSrchFld(self) -> Optional[EntryID]:
|
|
83
|
+
"""
|
|
84
|
+
The object EntryID of the Sent Items special folder of the linked
|
|
85
|
+
Message object.
|
|
86
|
+
"""
|
|
87
|
+
return EntryID.autoCreate(self.getStream('MailMsgAttSrchFld'))
|
|
88
|
+
|
|
89
|
+
@cached_property
|
|
90
|
+
def mailMsgAttSrchKey(self) -> Optional[bytes]:
|
|
91
|
+
"""
|
|
92
|
+
The search key for the linked message object, required only if
|
|
93
|
+
mailMsgAttMsg is None.
|
|
94
|
+
"""
|
|
95
|
+
return self.getStream('MailMsgAttSrchKey')
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def name(self) -> None:
|
|
99
|
+
# Doesn't save.
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def obj(self) -> None:
|
|
104
|
+
# No object to represent this.
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
registerHandler(LinkedObjectAttachment)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'OutlookImage',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
from typing import Optional, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from . import registerHandler
|
|
14
|
+
from .custom_handler import CustomAttachmentHandler
|
|
15
|
+
from ...enums import DVAspect, InsecureFeatures
|
|
16
|
+
from ...exceptions import DependencyError, SecurityError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..attachment_base import AttachmentBase
|
|
21
|
+
|
|
22
|
+
_ST_OLE = struct.Struct('<IIIII')
|
|
23
|
+
_ST_MAILSTREAM = struct.Struct('<III')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OutlookImageDIB(CustomAttachmentHandler):
|
|
27
|
+
"""
|
|
28
|
+
Custom handler for a special attachment type, a Device Independent Bitmap
|
|
29
|
+
stored in a way special to Outlook.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, attachment: AttachmentBase):
|
|
33
|
+
super().__init__(attachment)
|
|
34
|
+
# First we need to get the mailstream.
|
|
35
|
+
stream = self.getStream('\x03MailStream')
|
|
36
|
+
if not stream:
|
|
37
|
+
raise ValueError('MailStream could not be found.')
|
|
38
|
+
if len(stream) != 12:
|
|
39
|
+
raise ValueError('MailStream is the wrong length.')
|
|
40
|
+
# Next get the bitmap data.
|
|
41
|
+
self.__data = self.getStream('CONTENTS')
|
|
42
|
+
if not self.__data:
|
|
43
|
+
raise ValueError('Bitmap data could not be read for Outlook signature.')
|
|
44
|
+
# Get the OLE data.
|
|
45
|
+
oleStream = self.getStream('\x01Ole')
|
|
46
|
+
if not oleStream:
|
|
47
|
+
raise ValueError('OLE stream could not be found.')
|
|
48
|
+
|
|
49
|
+
# While I have only seen this stream be one length, it could in theory
|
|
50
|
+
# be more than one length. So long as it is *at least* 20 bytes, we
|
|
51
|
+
# call it valid.
|
|
52
|
+
if len(oleStream) < 20:
|
|
53
|
+
raise ValueError('OLE stream is too short.')
|
|
54
|
+
|
|
55
|
+
# Unpack and verify the OLE stream.
|
|
56
|
+
vals = _ST_OLE.unpack(oleStream[:20])
|
|
57
|
+
# Check the version magic.
|
|
58
|
+
if vals[0] != 0x2000001:
|
|
59
|
+
raise ValueError('OLE stream has wrong version magic.')
|
|
60
|
+
# Check the reserved bytes.
|
|
61
|
+
if vals[3] != 0:
|
|
62
|
+
raise ValueError('OLE stream has non-zero reserved int.')
|
|
63
|
+
|
|
64
|
+
# Unpack the mailstream and create the HTML tag.
|
|
65
|
+
vals = _ST_MAILSTREAM.unpack(stream)
|
|
66
|
+
self.__dvaspect = DVAspect(vals[0])
|
|
67
|
+
self.__x = vals[1]
|
|
68
|
+
self.__y = vals[2]
|
|
69
|
+
# Convert to twips for RTF.
|
|
70
|
+
self.__xtwips = int(round(self.__x / 1.7639))
|
|
71
|
+
self.__ytwips = int(round(self.__y / 1.7639))
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def isCorrectHandler(cls, attachment: AttachmentBase) -> bool:
|
|
75
|
+
if attachment.clsid != '00000316-0000-0000-C000-000000000046':
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
# Check for the required streams.
|
|
79
|
+
if not attachment.exists('__substg1.0_3701000D/CONTENTS'):
|
|
80
|
+
return False
|
|
81
|
+
if not attachment.exists('__substg1.0_3701000D/\x01Ole'):
|
|
82
|
+
return False
|
|
83
|
+
if not attachment.exists('__substg1.0_3701000D/\x03MailStream'):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
def generateRtf(self) -> Optional[bytes]:
|
|
89
|
+
"""
|
|
90
|
+
Generates the RTF to inject in place of the \\objattph tag.
|
|
91
|
+
|
|
92
|
+
If this function should do nothing, returns ``None``.
|
|
93
|
+
|
|
94
|
+
:raises DependencyError: PIL or Pillow could not be found.
|
|
95
|
+
"""
|
|
96
|
+
if InsecureFeatures.PIL_IMAGE_PARSING not in self.attachment.msg.insecureFeatures:
|
|
97
|
+
raise SecurityError('Generating the RTF for a custom attachment requires the insecure feature PIL_IMAGE_PARSING.')
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
import PIL.Image
|
|
101
|
+
except ImportError:
|
|
102
|
+
raise DependencyError('PIL or Pillow is required for inserting an Outlook Image into the body.')
|
|
103
|
+
|
|
104
|
+
# First, convert the bitmap into a PNG so we can insert it into the
|
|
105
|
+
# body.
|
|
106
|
+
import io
|
|
107
|
+
|
|
108
|
+
# Note, use self.data instead of self.__data to allow support for
|
|
109
|
+
# extensions.
|
|
110
|
+
with PIL.Image.open(io.BytesIO(self.data)) as img:
|
|
111
|
+
out = io.BytesIO()
|
|
112
|
+
img.save(out, 'PNG')
|
|
113
|
+
|
|
114
|
+
hexData = out.getvalue().hex()
|
|
115
|
+
|
|
116
|
+
inject = '{\\*\\shppict\n{\\pict\\picscalex100\\picscaley100'
|
|
117
|
+
inject += f'\\picw{img.width}\\pich{img.height}'
|
|
118
|
+
inject += f'\\picwgoal{self.__xtwips}\\pichgoal{self.__ytwips}\n'
|
|
119
|
+
inject += '\\pngblip ' + hexData + '}}'
|
|
120
|
+
|
|
121
|
+
return inject.encode()
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def data(self) -> bytes:
|
|
125
|
+
return self.__data
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def name(self) -> str:
|
|
129
|
+
# Try to get the name from the attachment. If that fails, name it based
|
|
130
|
+
# on the number.
|
|
131
|
+
if not (name := self.attachment.name):
|
|
132
|
+
name = f'attachment {int(self.attachment.dir[-8:], 16)}'
|
|
133
|
+
return name + '.bmp'
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def obj(self) -> bytes:
|
|
137
|
+
return self.data
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
registerHandler(OutlookImageDIB)
|