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