Skip to content

Commit 97d0111

Browse files
v1.2.4
1 parent 7efa24b commit 97d0111

13 files changed

+111
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
**v1.2.4**
2+
* Fixed issue with files with non-standard streams from custom attachment types. There were issues with viewing the streams in the tree as well as with opening them in the stream viewer.
3+
* Switched `HexViewer` from `QPlainTextEdit` to `QTextEdit` to allow for coloring to easily differentiate the headers from the actual data.
4+
* Added the ability to drag and drop an MSG file onto the window to open it.
5+
16
**v1.2.3**
27
* Added `python_requires` to setup.py.
38

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Credits
7777
.. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg
7878
:target: LICENSE.txt
7979

80-
.. |PyPI3| image:: https://img.shields.io/badge/pypi-1.2.3-blue.svg
81-
:target: https://pypi.org/project/msg-explorer/1.2.3/
80+
.. |PyPI3| image:: https://img.shields.io/badge/pypi-1.2.4-blue.svg
81+
:target: https://pypi.org/project/msg-explorer/1.2.4/
8282

8383
.. |PyPI2| image:: https://img.shields.io/badge/python-3.6+-brightgreen.svg
8484
:target: https://www.python.org/downloads/release/python-367/

msg-explorer.pyproject

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"files": ["msg_explorer/main.py","msg_explorer/utils.py","msg_explorer/guid_viewer.py","msg_explorer/string_viewer.py","msg_explorer/properties_viewer.py","msg_explorer/multiple_binary_viewer.py","msg_explorer/ui/guid_viewer.ui","msg_explorer/ui/multiple_binary_viewer.ui","msg_explorer/_recompile.py","msg_explorer/multiple_viewer.py","msg_explorer/ui/stream_viewer.ui","msg_explorer/ui/named_properties_viewer.ui","msg_explorer/ui/string_viewer.ui","msg_explorer/msg_details_page.py","msg_explorer/ui/logger_widget.ui","msg_explorer/ui/unhandled_exception.ui","msg_explorer/main_window.py","msg_explorer/__main__.py","msg_explorer/attachments_browser.py","msg_explorer/ui/msg_tree_viewer.ui","msg_explorer/ui/properties_viewer.ui","msg_explorer/ui/__init__.py","msg_explorer/ui/main_window.ui","msg_explorer/logger.py","msg_explorer/ui/attachments_browser.ui","msg_explorer/font_handler.py","msg_explorer/ui/multiple_viewer.ui","msg_explorer/ui/msg_details_page.ui","requirements.txt","msg_explorer/stream_viewer.py","msg_explorer/__init__.py","msg_explorer/logger_widget.py","main.py","msg_explorer/ui/loading_screen.ui","msg_explorer/ui/hex_viewer.ui","msg_explorer/named_properties_viewer.py","msg_explorer/msg_tree_viewer.py","msg_explorer/hex_viewer.py","msg_explorer/app_icons.qrc"]
2+
"files": ["msg_explorer/main.py","msg_explorer/utils.py","msg_explorer/guid_viewer.py","msg_explorer/string_viewer.py","msg_explorer/properties_viewer.py","msg_explorer/multiple_binary_viewer.py","msg_explorer/ui/guid_viewer.ui","msg_explorer/ui/multiple_binary_viewer.ui","msg_explorer/_recompile.py","msg_explorer/multiple_viewer.py","msg_explorer/app_icons.qrc","msg_explorer/ui/stream_viewer.ui","msg_explorer/ui/named_properties_viewer.ui","msg_explorer/ui/string_viewer.ui","msg_explorer/msg_details_page.py","msg_explorer/ui/logger_widget.ui","msg_explorer/ui/unhandled_exception.ui","msg_explorer/main_window.py","msg_explorer/__main__.py","msg_explorer/attachments_browser.py","msg_explorer/ui/msg_tree_viewer.ui","msg_explorer/ui/properties_viewer.ui","msg_explorer/ui/__init__.py","msg_explorer/ui/main_window.ui","msg_explorer/logger.py","msg_explorer/ui/attachments_browser.ui","msg_explorer/font_handler.py","msg_explorer/ui/multiple_viewer.ui","msg_explorer/ui/msg_details_page.ui","requirements.txt","msg_explorer/stream_viewer.py","msg_explorer/__init__.py","msg_explorer/logger_widget.py","main.py","msg_explorer/ui/loading_screen.ui","msg_explorer/ui/hex_viewer.ui","msg_explorer/named_properties_viewer.py","msg_explorer/msg_tree_viewer.py","msg_explorer/hex_viewer.py","msg_explorer/constants.py"]
33
}

msg_explorer/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2828

2929
__author__ = 'Destiny Peterson'
30-
__date__ = '2022-06-17'
31-
__version__ = '1.2.3'
30+
__date__ = '2022-06-18'
31+
__version__ = '1.2.4'
3232

3333
# When this module is imported, we should try to compile the forms. They only
3434
# compile when they are outdated.

msg_explorer/attachments_browser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
logger = logging.getLogger(__name__)
1515
logger.addHandler(logging.NullHandler())
1616

17+
1718
class AttachmentsBrowser(QtWidgets.QWidget):
1819
# Signals that an attachment was double clicked.
1920
attachmentSelected = Signal(int)

msg_explorer/constants.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import re
2+
import string
3+
4+
5+
# This expression will match any non-ascii character.
6+
RE_SPECIAL_CHAR = re.compile(f'[^{string.printable}]|[\t\n\r\x0B\x0C]')
7+
8+
# This expression will match any standard file name in an MSG file.
9+
# For files that are inside of custom attachment types, this will
10+
# not match.
11+
RE_STANDARD_FILE = re.compile(r'^(__properties_version1\.0)|(__substg1\.0_[0-9a-fA-F]{8}(\-[0-9a-fA-F]{8})?)$')
12+
13+
# This expression will match any standard folder name in an MSG file.
14+
RE_STANDARD_FOLDER = re.compile(r'^(__attach_version1\.0_#[0-9a-fA-F]{8})|(__nameid_version1\.0)|(__recip_version1\.0_#[0-9a-fA-F]{8})$')

msg_explorer/hex_viewer.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
_CHARS = (
1414
'.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
1515
'.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
16-
' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/',
17-
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
16+
' ', '!', '&quot;', '#', '$', '%', '&amp;', '&apos;', '(', ')', '*', '+', ',', '-', '.', '/',
17+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '&lt;', '=', '&gt;', '?',
1818
'@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
1919
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_',
2020
'`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
@@ -58,6 +58,10 @@ def loadHexData(self, data):
5858
if lines:
5959
if len(lines[-1]) != 16:
6060
lines[-1] += [' '] * (16 - len(lines[-1]))
61-
finalHexData = 'Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F Decoded Text\n'
62-
finalHexData += '\n'.join(f'{index:08X} {" ".join(line)} {rawDataLines[index]}' for index, line in enumerate(lines))
63-
self.ui.hexViewer.setPlainText(finalHexData)
61+
62+
# First setup the start of the data.
63+
finalHexData = '<html><head><style>span { color: #0000AA; }</style></head><body>'
64+
finalHexData += '<span>Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F Decoded Text</span>\n<br>'.replace(' ', '&nbsp;')
65+
finalHexData += '<br>\n'.join(f'<span>{index:08X}</span> {" ".join(line)} {rawDataLines[index]}' for index, line in enumerate(lines)).replace(' ', '&nbsp;')
66+
finalHexData += '</body></html>'
67+
self.ui.hexViewer.setHtml(finalHexData)

msg_explorer/main_window.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# This Python file uses the following encoding: utf-8
22
import os
3+
import pathlib
34
import threading
45

56
import extract_msg
@@ -83,6 +84,20 @@ def closeEvent(self, event):
8384
"""
8485
QApplication.closeAllWindows()
8586

87+
def dragEnterEvent(self, event):
88+
# Handle a file being drag and dropped onto this window.
89+
mime = event.mimeData()
90+
if mime.hasUrls() and len(mime.urls()) == 1 and mime.urls()[0].isLocalFile():
91+
# We know it has exactly one local url, so let's check it.
92+
f = pathlib.Path(mime.urls()[0].toLocalFile())
93+
if f.suffix.lower() == '.msg' and f.is_file():
94+
event.acceptProposedAction()
95+
96+
def dropEvent(self, event):
97+
if QMessageBox.question(self, 'Open Msg?', 'Are you sure you want to open this MSG file?') == QMessageBox.Yes:
98+
event.acceptProposedAction()
99+
self.loadMsgFile(event.mimeData().urls()[0].toLocalFile())
100+
86101
@Slot(int)
87102
def attachmentSelected(self, index):
88103
if isinstance(self.__msg, extract_msg.MessageSignedBase):
@@ -118,12 +133,15 @@ def closeFile(self):
118133
self.ui.actionLoad_Parent_Msg.setEnabled(False)
119134

120135
@Slot()
121-
def loadMsgFile(self):
136+
def loadMsgFile(self, path = None):
122137
"""
123138
Brings up a dialog to load a specific MSG file.
124139
"""
125-
msgPath = QFileDialog.getOpenFileName(filter = self.tr('MSG Files (*.msg)'))
126-
if msgPath[0]:
140+
if path:
141+
msgPath = path
142+
else:
143+
msgPath = QFileDialog.getOpenFileName(filter = self.tr('MSG Files (*.msg)'))[0]
144+
if msgPath:
127145
# Create a popup for the loading screen.
128146
loadingScreenWidget = QWidget()
129147
loadingScreen = Ui_LoadingScreen()
@@ -170,7 +188,7 @@ def loadParent(self):
170188

171189
def _loadMsgThread(self, msgPath, output):
172190
try:
173-
msgFile = extract_msg.openMsg(msgPath[0], attachmentErrorBehavior = extract_msg.enums.AttachErrorBehavior.BROKEN, strict = False)
191+
msgFile = extract_msg.openMsg(msgPath, attachmentErrorBehavior = extract_msg.enums.AttachErrorBehavior.BROKEN, strict = False)
174192
output[0] = msgFile
175193
except Exception as e:
176194
output[0] = e

msg_explorer/msg_tree_viewer.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from PySide6.QtCore import Signal, SIGNAL, Slot, SLOT
1010
from PySide6.QtWidgets import QTreeWidgetItem
1111

12-
from . import utils
12+
from . import constants, utils
1313
from .ui.ui_msg_tree_viewer import Ui_MSGTreeViewer
1414

1515

@@ -21,6 +21,9 @@ class _DataTypeEnum(enum.Enum):
2121

2222
class MsgTreeItem(QTreeWidgetItem):
2323
def __init__(self, text : str, _type : _DataTypeEnum):
24+
# First thing is first, check the text to see if it needs to be corrected.
25+
self.__rawText = text
26+
text = constants.RE_SPECIAL_CHAR.sub(lambda match : f'[{ord(match.group())}]', text)
2427
super().__init__((text,))
2528
self.__entryType = _type
2629

@@ -35,6 +38,10 @@ def __lt__(self, treeItem : "MsgTreeItem"):
3538
def entryType(self) -> _DataTypeEnum:
3639
return self.__entryType
3740

41+
@property
42+
def rawText(self) -> str:
43+
return self.__rawText
44+
3845

3946

4047
class MSGTreeViewer(QtWidgets.QWidget):
@@ -105,10 +112,10 @@ def _treeItemDoubleClicked(self, item, column):
105112
"""
106113
# Check if the item clicked was a stream. If it was, then continue.
107114
if item.entryType == _DataTypeEnum.FILE:
108-
path = [item.data(0, 0)]
115+
path = [item.rawText]
109116
while item.parent():
110117
item = item.parent()
111-
path.insert(0, item.data(0, 0))
118+
path.insert(0, item.rawText)
112119

113120
self.fileDoubleClicked.emit(path)
114121

msg_explorer/stream_viewer.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# This Python file uses the following encoding: utf-8
22
import copy
3+
import logging
34
import sys
45

56
import extract_msg
@@ -8,10 +9,14 @@
89
from PySide6 import QtWidgets
910
from PySide6.QtCore import Signal, SIGNAL, Slot, SLOT
1011

11-
from . import utils
12+
from . import constants, utils
1213
from .ui.ui_stream_viewer import Ui_StreamViewer
1314

1415

16+
logger = logging.getLogger(__name__)
17+
logger.addHandler(logging.NullHandler())
18+
19+
1520
class StreamViewer(QtWidgets.QWidget):
1621
def __init__(self, parent = None):
1722
super().__init__(parent)
@@ -119,26 +124,34 @@ def openStream(self, name, prefix = True):
119124
else:
120125
_type = name[-1][-4:]
121126
path = name[:-1] + [name[-1][:-4]]
122-
data = self.__msg._getTypedStream(path, False, _type)[1]
123-
if _type in ('001E', '001F'): # String.
124-
self.ui.pageParsedString.loadString(data, _type)
125-
elif _type == '0048': # GUID.
126-
self.ui.pageParsedGuidViewer.loadGuid(data)
127-
elif _type == '0102': # Binary.
128-
# For binary, we just show the hex viewer.
129-
pass
130-
elif _type == '1102': # Multiple Binary.
131-
# For multiple binary we load a page with a list
132-
# of entries and a hex viewer that shows the
133-
# currently selected one.
134-
self.ui.pageParsedMultipleBinary.loadMultiple(data)
135-
elif _type.startswith('1'): # Other multiples.
136-
self.ui.pageParsedMultiple.loadMultiple(data, _type)
137-
138-
try:
139-
self.__currentPage = self.__typePages[_type]
140-
except KeyError as e:
141-
utils.displayException(e)
127+
if constants.RE_STANDARD_FILE.match(name[-1]):
128+
data = self.__msg._getTypedStream(path, False, _type)[1]
129+
if _type in ('001E', '001F'): # String.
130+
self.ui.pageParsedString.loadString(data, _type)
131+
elif _type == '0048': # GUID.
132+
self.ui.pageParsedGuidViewer.loadGuid(data)
133+
elif _type == '0102': # Binary.
134+
# For binary, we just show the hex viewer.
135+
pass
136+
elif _type == '1102': # Multiple Binary.
137+
# For multiple binary we load a page with a list
138+
# of entries and a hex viewer that shows the
139+
# currently selected one.
140+
self.ui.pageParsedMultipleBinary.loadMultiple(data)
141+
elif _type.startswith('1'): # Other multiples.
142+
self.ui.pageParsedMultiple.loadMultiple(data, _type)
143+
144+
# First, check to make sure we are not opening a special custom attachment
145+
# file. If we are, this regular expression won't match it, and we just
146+
# treat it as binary data.
147+
if constants.RE_STANDARD_FILE.match(name[-1]):
148+
try:
149+
self.__currentPage = self.__typePages[_type]
150+
except KeyError as e:
151+
utils.displayException(e)
152+
else:
153+
logger.info(f'Attempting to view non-standard stream "{"/".join(name)}". Interpretting as plain binary data.')
154+
self.__currentPage = self.ui.pageHexViewer
142155
self._changeViewType()
143156

144157
@Slot(str, bytes)

0 commit comments

Comments
 (0)