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,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)