Create Gui-MSG-Viewer.py

This commit is contained in:
ghostersk
2025-03-16 21:34:23 +00:00
committed by GitHub
parent 10d73d5306
commit 552289b027

View File

@@ -0,0 +1,512 @@
from PyQt6.QtWidgets import ( QApplication, QMainWindow,
QMessageBox, QTextEdit, QListWidgetItem, QMenuBar,
QTabWidget, QVBoxLayout, QWidget, QFileDialog,
QListWidget, QDialog, QPushButton, QPlainTextEdit, QProgressDialog
)
from PyQt6.QtWebEngineWidgets import QWebEngineView # For HTML rendering
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtCore import Qt, QEvent
import sys
import os
import extract_msg
import re
# pip install PyQt6-WebEngine PyQt6 extract-msg
application_name = "MSG Viewer"
replacement = str.maketrans({'<': '&lt;', '>': '&gt;'}) # Proper HTML escaping
icon_file = "app_icon.ico"
# --- Functions ---
def check_folder_path(folder_location):
"""Ensure the folder exists, create it if it doesnt."""
if not os.path.isdir(folder_location):
os.makedirs(folder_location)
return f"Path {folder_location} now exists."
def sanitize_name(name):
"""Sanitize a string to keep only letters, numbers, and basic punctuation; replace spaces with underscores."""
try:
if not name:
return "Untitled"
name = name.strip().replace(" ", "_")
sanitized = re.sub(r'[^\w.,-]', '', name).strip('_')
return sanitized if sanitized else "Unnamed"
except Exception as e:
QMessageBox.critical(None, f"Error while cleaning subject name: {e}")
def extract_attachments(email_msg):
try:
"""Extract attachments into memory and return message content with metadata."""
attachment_info = {}
saved_count = 0
for attachment in email_msg.attachments:
if not attachment.contentId:
attach_name = attachment.longFilename or attachment.shortFilename or f"Unnamed_Attachment_{saved_count}"
sanitized_attach_name = sanitize_name(attach_name)
attachment_info[sanitized_attach_name] = (attachment, attachment.data)
saved_count += 1
line_break = "<br>" if email_msg.htmlBody else "\n"
sender = email_msg.sender or "Unknown Sender"
sender = sender.translate(replacement)
to = ", ".join(recipient.formatted for recipient in email_msg.recipients) or "Unknown Recipient"
to = to.translate(replacement)
metadata = (
f"From: {sender}{line_break}"
f"To: {to}{line_break}"
f"Subject: {email_msg.subject}{line_break}"
)
cc = ", ".join(recipient.email for recipient in email_msg.recipients if recipient.type == "cc")
if cc:
metadata += f"CC: {cc}{line_break}"
bcc = ", ".join(recipient.email for recipient in email_msg.recipients if recipient.type == "bcc")
if bcc:
metadata += f"BCC: {bcc}{line_break}"
date_str = email_msg.date.strftime("%d. %b %Y %H:%M")
metadata += f"Date: {date_str}{line_break}{'_' * 65}{line_break}"
attachments_list = ""
if saved_count > 0:
attachments_list = f"List of email attachments:{line_break}"
for attach_name in attachment_info.keys():
attachments_list += f"- {attach_name}{line_break}"
attachments_list += f"{'_' * 65}{line_break}"
message_content = email_msg.htmlBody.decode('utf-8') if email_msg.htmlBody else email_msg.body if email_msg.body else "No content available."
full_content = metadata + attachments_list + message_content
return full_content, saved_count, attachment_info
except Exception as e:
QMessageBox.critical(None, f"Error exporting attachment: {e}")
def batch_extract_emails(folder_path, parent=None):
"""Extract all .msg files from a folder to Downloads/exported_emails with progress feedback."""
try:
downloads_dir = os.path.expanduser("~/Downloads")
export_base = os.path.join(downloads_dir, "exported_emails")
check_folder_path(export_base)
# Collect all .msg files
msg_files = []
for root, _, files in os.walk(folder_path):
for file in files:
if file.lower().endswith(".msg"):
msg_files.append(os.path.join(root, file))
if not msg_files:
QMessageBox.information(parent, "Batch Export", "No .msg files found in the selected folder.")
return
total_files = len(msg_files)
success_count = 0
failed_files = []
# Single progress dialog for both progress and result
progress = QProgressDialog("Exporting emails...", "Cancel", 0, total_files, parent)
progress.setWindowTitle("Batch Export Progress")
progress.setWindowModality(Qt.WindowModality.WindowModal)
progress.setMinimumDuration(0)
progress.setAutoClose(False) # Keep open until explicitly closed
def process_next_email(index=0):
nonlocal success_count, failed_files
if progress.wasCanceled() or index >= total_files:
# Update the dialog with final result
message = f"Batch export completed!\n\nSuccessfully exported: {success_count}/{total_files} files\n"
if failed_files:
message += "Failed files:\n" + "\n".join(f"- {os.path.basename(f)}" for f in failed_files[:5])
if len(failed_files) > 5:
message += f"\n...and {len(failed_files) - 5} more"
message += f"\nExport location: {export_base}"
progress.setLabelText(message)
progress.setRange(0, 100) # Switch to percentage
progress.setValue(100) # Indicate completion
progress.setCancelButtonText("Ok") # Rename Cancel to Ok
progress.addAction
progress.canceled.connect(progress.close) # Close on Ok
return
progress.setValue(index)
progress.setLabelText(f"Exporting {os.path.basename(msg_files[index])} ({index + 1}/{total_files})")
msg_path = msg_files[index]
try:
msg = extract_msg.Message(msg_path)
subject_folder = os.path.join(export_base, sanitize_name(msg.subject))
check_folder_path(subject_folder)
content, saved_count, attachment_info = extract_attachments(msg)
for attach_name, (attachment, data) in attachment_info.items():
attach_path = os.path.join(subject_folder, sanitize_name(attach_name))
with open(attach_path, 'wb') as f:
f.write(data)
pdf_path = os.path.join(subject_folder, f"aMSG-{sanitize_name(msg.subject)}.pdf")
# Create hidden QWebEngineView
temp_view = QWebEngineView()
temp_view.setVisible(False)
temp_view.setHtml(content if msg.htmlBody else f"<pre>{content}</pre>")
def on_load_finished(ok):
if ok:
def on_pdf_finished(success):
nonlocal success_count, failed_files
if success:
success_count += 1
print(f"Extracted {os.path.basename(msg_path)} to {subject_folder}")
else:
failed_files.append(msg_path)
print(f"Failed to export PDF for {os.path.basename(msg_path)}")
temp_view.deleteLater()
process_next_email(index + 1)
temp_view.page().printToPdf(pdf_path)
temp_view.page().pdfPrintingFinished.connect(on_pdf_finished)
else:
failed_files.append(msg_path)
print(f"Failed to load HTML for {os.path.basename(msg_path)}")
temp_view.deleteLater()
process_next_email(index + 1)
temp_view.loadFinished.connect(on_load_finished)
except Exception as e:
failed_files.append(msg_path)
print(f"Error extracting {msg_path}: {e}")
process_next_email(index + 1)
# Start processing
process_next_email(0)
except Exception as e:
QMessageBox.critical(parent, "Batch Export Error", f"Error during batch export: {e}")
print(f"Batch export error: {e}")
# --- Attachment Dialog ---
class AttachmentDialog(QDialog):
def __init__(self, attachment_info, subject, parent=None):
super().__init__(parent)
self.attachment_info = attachment_info
self.subject = subject
self.initUI()
def initUI(self):
self.setWindowTitle("Attachments")
self.setGeometry(100, 100, 400, 300)
layout = QVBoxLayout(self)
self.attachments_list = QListWidget(self)
for attach_name, (attachment, data) in self.attachment_info.items():
item = QListWidgetItem(attach_name)
item.setData(Qt.ItemDataRole.UserRole, (attachment, data))
self.attachments_list.addItem(item)
self.attachments_list.itemDoubleClicked.connect(self.export_attachment)
layout.addWidget(self.attachments_list)
self.export_all_btn = QPushButton("Export All", self)
self.export_all_btn.clicked.connect(self.export_all_attachments)
layout.addWidget(self.export_all_btn)
self.setLayout(layout)
def export_attachment(self, item):
attachment, data = item.data(Qt.ItemDataRole.UserRole)
attach_name = item.text()
dest_file, _ = QFileDialog.getSaveFileName(self, "Save Attachment", attach_name, "All Files (*)")
if dest_file:
try:
with open(dest_file, 'wb') as f:
f.write(data)
msg_box = QMessageBox()
msg_box.setWindowTitle("Export Result")
msg_box.setText(f"Attachment '{attach_name}' exported successfully to:\n{dest_file}")
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Open)
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
result = msg_box.exec()
if result == QMessageBox.StandardButton.Open:
os.startfile(dest_file) if os.name == 'nt' else os.system(f"open {dest_file}" if sys.platform == "darwin" else f"xdg-open {dest_file}")
print(f"Attachment exported to {dest_file}")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Error exporting attachment '{attach_name}':\n{str(e)}")
print(f"Error exporting attachment: {e}")
def export_all_attachments(self):
folder = QFileDialog.getExistingDirectory(self, "Select Export Folder")
if folder:
try:
subject_folder = os.path.join(folder, sanitize_name(self.subject))
check_folder_path(subject_folder)
exported_count = 0
for attach_name, (attachment, data) in self.attachment_info.items():
attach_path = os.path.join(subject_folder, sanitize_name(attach_name))
with open(attach_path, 'wb') as f:
f.write(data)
exported_count += 1
msg_box = QMessageBox()
msg_box.setWindowTitle("Export Result")
msg_box.setText(f"All {exported_count} attachments exported successfully to:\n{subject_folder}")
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Open)
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
result = msg_box.exec()
if result == QMessageBox.StandardButton.Open:
os.startfile(subject_folder) if os.name == 'nt' else os.system(f"open {subject_folder}" if sys.platform == "darwin" else f"xdg-open {subject_folder}")
print(f"All attachments exported to {subject_folder}")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Error exporting all attachments:\n{str(e)}")
print(f"Error exporting all attachments: {e}")
# --- Header Dialog ---
class HeadersDialog(QDialog):
def __init__(self, header_info=None, parent=None):
super().__init__(parent)
self.header = header_info
self.initUI()
def initUI(self):
self.setWindowTitle("Header")
self.setGeometry(100, 100, 600, 400)
layout = QVBoxLayout(self)
self.header_text = QPlainTextEdit(self)
self.header_text.setPlainText(self.header if self.header else "No header available")
self.header_text.setReadOnly(True)
layout.addWidget(self.header_text)
self.copy_btn = QPushButton("Copy to Clipboard", self)
self.copy_btn.clicked.connect(self.copy_header)
layout.addWidget(self.copy_btn)
self.setLayout(layout)
def copy_header(self):
try:
clipboard = QApplication.clipboard()
clipboard.setText(self.header)
except Exception as e:
QMessageBox.critical(self, f"Error copying email header to clipboard: {e}")
# --- GUI Classes ---
class TabContent(QWidget):
def __init__(self, msg=None, content="", attachment_info=None, header_info=None):
super().__init__()
self.msg = msg
self.content = content
self.attachment_info = attachment_info or {}
self.header_info = header_info
self.initUI()
def initUI(self):
layout = QVBoxLayout(self)
if self.msg and self.msg.htmlBody:
self.content_area = QWebEngineView(self)
self.content_area.setHtml(self.content)
else:
self.content_area = QTextEdit(self)
self.content_area.setObjectName("Container")
self.content_area.setStyleSheet(
"""#Container {
background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
border-radius: 5px;
}"""
)
self.content_area.setPlainText(self.content)
self.content_area.setReadOnly(True)
layout.addWidget(self.content_area, stretch=1)
if self.attachment_info:
self.view_attachments_btn = QPushButton("View Attachments", self)
self.view_attachments_btn.clicked.connect(self.show_attachments)
layout.addWidget(self.view_attachments_btn, stretch=0)
self.setLayout(layout)
def show_attachments(self):
dialog = AttachmentDialog(self.attachment_info, self.msg.subject, self)
dialog.exec()
class AppMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.tabs = QTabWidget(self)
self.tabs.setTabsClosable(True)
self.tabs.setMovable(True)
self.tabs.tabCloseRequested.connect(self.close_tab)
self.tabs.currentChanged.connect(self.on_tab_changed)
self.setCentralWidget(self.tabs)
self.tabs.tabBar().installEventFilter(self)
self.setWindowTitle(application_name)
self.setGeometry(100, 100, 800, 600)
self.setWindowIcon(QIcon(icon_file))
self.createToolbar()
def createToolbar(self):
self.menuBar = QMenuBar()
self.setMenuBar(self.menuBar)
self.open_btn = QAction("Open", self)
self.open_btn.triggered.connect(self.open_file)
self.open_btn.setStatusTip("Open MSG file")
self.menuBar.addAction(self.open_btn)
self.header_btn = QAction("Header", self)
self.header_btn.triggered.connect(self.show_header)
self.header_btn.setStatusTip("View email header")
self.header_btn.setVisible(False)
self.menuBar.addAction(self.header_btn)
self.export_btn = QAction("Export", self)
self.export_btn.setStatusTip("Export email and attachments")
self.export_btn.triggered.connect(self.export_file)
self.export_btn.setVisible(False)
self.menuBar.addAction(self.export_btn)
self.batch_extract_btn = QAction("Batch Extract", self)
self.batch_extract_btn.triggered.connect(self.batch_extract)
self.batch_extract_btn.setStatusTip("Extract all .msg files from a folder")
self.menuBar.addAction(self.batch_extract_btn)
self.menuBar.setStyleSheet("""
QMenuBar {
background-color: #9a9c9a;
border-bottom: 1px solid #000000;
padding: 2px;
}
QMenuBar::item {
background-color: #292626;
padding: 5px 10px;
border: 1px solid transparent;
margin: 2px;
font-weight: bold;
}
QMenuBar::item:selected {
background-color: #d0d0d0;
border: 1px solid #999999;
color: black;
border-radius: 3px;
}
QMenuBar::item:pressed {
background-color: #b0b0b0;
}
""")
def show_header(self):
try:
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'header_info'):
header_dialog = HeadersDialog(str(current_tab.header_info), self)
header_dialog.exec()
except Exception as e:
QMessageBox.critical(self, f"Error getting headers: {e}")
def open_file(self):
file, _ = QFileDialog.getOpenFileName(self, "Open MSG File", "", "MSG Files (*.msg);;All Files (*)")
if file:
try:
msg = extract_msg.Message(file)
content, _, attachment_info = extract_attachments(msg)
header_info = msg.header if msg.header else "No header available"
tab_content = TabContent(msg, content, attachment_info, header_info)
tab_index = self.tabs.addTab(tab_content, os.path.basename(file))
self.tabs.setCurrentIndex(tab_index)
self.export_btn.setVisible(True)
self.header_btn.setVisible(True)
except Exception as e:
QMessageBox.critical(self, f"Error opening file: {e}")
print(f"Error opening file: {e}")
def export_file(self):
current_tab = self.tabs.currentWidget()
if not current_tab or not current_tab.msg:
return
folder = QFileDialog.getExistingDirectory(self, "Select Export Folder")
if folder:
try:
content, saved_count, attachment_info = extract_attachments(current_tab.msg)
subject_folder = os.path.join(folder, sanitize_name(current_tab.msg.subject))
check_folder_path(subject_folder)
for attach_name, (attachment, data) in attachment_info.items():
attach_path = os.path.join(subject_folder, sanitize_name(attach_name))
with open(attach_path, 'wb') as f:
f.write(data)
pdf_path = os.path.join(subject_folder, f"aMSG-{sanitize_name(current_tab.msg.subject)}.pdf")
def on_pdf_finished(success):
if success:
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Export Result")
msg_box.setText(f"Email and {saved_count} attachments exported successfully to:\n{subject_folder}\nPDF saved as: {os.path.basename(pdf_path)}")
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Open)
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
result = msg_box.exec()
if result == QMessageBox.StandardButton.Open:
os.startfile(pdf_path) if os.name == 'nt' else os.system(f"open {pdf_path}" if sys.platform == "darwin" else f"xdg-open {pdf_path}")
print(f"Exported email as PDF and {saved_count} attachments to {subject_folder}")
else:
QMessageBox.critical(self, "Export Error", "Failed to save PDF")
if isinstance(current_tab.content_area, QWebEngineView):
current_tab.content_area.page().printToPdf(pdf_path)
current_tab.content_area.page().pdfPrintingFinished.connect(on_pdf_finished)
else:
temp_view = QWebEngineView()
temp_view.setHtml(f"<pre>{content}</pre>")
temp_view.page().printToPdf(pdf_path)
temp_view.page().pdfPrintingFinished.connect(on_pdf_finished)
except Exception as e:
QMessageBox.critical(self, f"Error exporting: {e}")
print(f"Error exporting: {e}")
def batch_extract(self):
folder = QFileDialog.getExistingDirectory(self, "Select Folder with MSG Files")
if folder:
batch_extract_emails(folder, self)
def close_tab(self, index):
try:
widget = self.tabs.widget(index)
if widget:
self.tabs.removeTab(index)
widget.deleteLater()
except Exception as e:
QMessageBox.critical(self, f"Error Closing tab: {e}")
def on_tab_changed(self, index):
try:
current_tab = self.tabs.widget(index)
self.export_btn.setVisible(bool(current_tab and current_tab.msg))
self.header_btn.setVisible(bool(current_tab and current_tab.msg))
except Exception as e:
QMessageBox.critical(self, f"Error Changing tabs: {e}")
def eventFilter(self, source, event):
if source == self.tabs.tabBar() and event.type() == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.MiddleButton:
index = self.tabs.tabBar().tabAt(event.pos())
if index != -1:
self.tabs.removeTab(index)
return True
return super().eventFilter(source, event)
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
app.setWindowIcon(QIcon(icon_file))
app_window = AppMainWindow()
app_window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()