""" msg_to_clipboard.py Převede Outlook .msg email (i vlákno odpovědí) do formátu: DD.MM.YYYY HH:MM od Jméno: text a zkopíruje do schránky. Použití: python msg_to_clipboard.py "cesta/k/souboru.msg" nebo spustit bez argumentu -> otevře dialog pro výběr souboru """ import sys import re import tkinter as tk from tkinter import filedialog, messagebox import extract_msg from bs4 import BeautifulSoup from dateutil import parser as dateparser def decode_html(html_bytes: bytes) -> str: """Dekóduje HTML bytes – zkouší UTF-8 přísně (selže na špatných sekvencích), pak záložní windows-1250 a latin-1.""" for enc in ("utf-8", "windows-1250", "latin-1"): try: return html_bytes.decode(enc) # utf-8 je přísné – selže pro non-UTF-8 except (UnicodeDecodeError, LookupError): continue return html_bytes.decode("utf-8", errors="replace") def clean_text(text: str) -> str: """Smaže zbytečné bílé znaky a nbsp, vrátí jeden řádek.""" text = text.replace("\xa0", " ") text = re.sub(r"\s+", " ", text) return text.strip() def is_separator(element) -> bool: """Vrátí True pokud je element Outlook oddělovač citace (border-top div).""" if getattr(element, "name", None) != "div": return False inner = element.find("div", style=lambda s: s and "border-top" in s) return inner is not None def parse_separator(element) -> tuple[str, str | None]: """Z oddělovače vytáhne jméno odesílatele a řetězec data.""" text = element.get_text(separator="\n") from_match = re.search(r"From:\s*(.+?)(?:\n|$)", text) sent_match = re.search(r"Sent:\s*(.+?)(?:\n|$)", text) sender_raw = from_match.group(1).strip() if from_match else "Neznámý" # "Jméno Příjmení " → "Jméno Příjmení" name_only = re.match(r"^(.+?)\s*<", sender_raw) sender = name_only.group(1).strip() if name_only else sender_raw sent_str = sent_match.group(1).strip() if sent_match else None return sender, sent_str def parse_date(sent_str: str | None, fallback=None): if not sent_str: return fallback try: return dateparser.parse(sent_str) except Exception: return fallback def extract_sections(soup, main_date, main_sender: str) -> list[dict]: """ Projde HTML tělo a rozdělí ho na sekce (každá zpráva ve vláknu). Vrátí seznam diktů: {'sender', 'date', 'text'} seřazených od nejstaršího. """ body_div = soup.find("div", class_="WordSection1") or soup.body if not body_div: return [] sections = [] current = {"sender": main_sender, "date": main_date, "parts": []} for child in body_div.children: if is_separator(child): # Ulož aktuální sekci a začni novou text = clean_text(" ".join(current["parts"])) sections.append({ "sender": current["sender"], "date": current["date"], "text": text, }) sender, sent_str = parse_separator(child) current = { "sender": sender, "date": parse_date(sent_str), "parts": [], } elif hasattr(child, "get_text"): t = clean_text(child.get_text(separator=" ")) if t: current["parts"].append(t) # Poslední sekce text = clean_text(" ".join(current["parts"])) sections.append({ "sender": current["sender"], "date": current["date"], "text": text, }) # Otočit – v e-mailu je nejnovější nahoře, chceme chronologicky sections.reverse() return sections def parse_msg(msg_path: str) -> list[dict]: msg = extract_msg.openMsg(msg_path) main_date = msg.date # Sender: preferuj display name před celým polem main_sender = msg.sender or "Neznámý" name_only = re.match(r"^(.+?)\s*<", main_sender) if name_only: main_sender = name_only.group(1).strip() if msg.htmlBody: html_text = decode_html(msg.htmlBody) soup = BeautifulSoup(html_text, "html.parser") return extract_sections(soup, main_date, main_sender) # Fallback – prostý text return [{ "sender": main_sender, "date": main_date, "text": clean_text((msg.body or "").replace("\r\n", " ")), }] def format_sections(sections: list[dict]) -> str: lines = [] for s in sections: dt = s["date"] if dt: date_str = dt.strftime("%d.%m.%Y %H:%M") else: date_str = "??" lines.append(f"{date_str} od {s['sender']}: {s['text']}") return "\n".join(lines) def copy_to_clipboard(text: str): root = tk.Tk() root.withdraw() root.clipboard_clear() root.clipboard_append(text) root.update() root.after(2000, root.destroy) root.mainloop() def show_result(text: str): """Zobrazí výsledek v malém okně a zkopíruje do schránky.""" root = tk.Tk() root.title("Email → schránka") root.resizable(True, True) frame = tk.Frame(root, padx=10, pady=10) frame.pack(fill=tk.BOTH, expand=True) label = tk.Label(frame, text="Zkopírováno do schránky. Náhled:", anchor="w") label.pack(fill=tk.X) text_widget = tk.Text(frame, wrap=tk.WORD, height=12, width=80) text_widget.insert(tk.END, text) text_widget.config(state=tk.DISABLED) text_widget.pack(fill=tk.BOTH, expand=True, pady=(4, 0)) scrollbar = tk.Scrollbar(frame, command=text_widget.yview) text_widget.config(yscrollcommand=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) def copy_again(): root.clipboard_clear() root.clipboard_append(text) root.update() btn_frame = tk.Frame(frame) btn_frame.pack(fill=tk.X, pady=(8, 0)) tk.Button(btn_frame, text="Kopírovat znovu", command=copy_again).pack(side=tk.LEFT) tk.Button(btn_frame, text="Zavřít", command=root.destroy).pack(side=tk.RIGHT) # Auto-copy on open root.clipboard_clear() root.clipboard_append(text) root.update() root.mainloop() def main(): if len(sys.argv) >= 2: msg_path = sys.argv[1] else: root = tk.Tk() root.withdraw() msg_path = filedialog.askopenfilename( title="Vyberte email (.msg)", filetypes=[("Outlook Email", "*.msg"), ("Všechny soubory", "*.*")], ) root.destroy() if not msg_path: return try: sections = parse_msg(msg_path) result = format_sections(sections) except Exception as exc: messagebox.showerror("Chyba", f"Nepodařilo se zpracovat email:\n{exc}") return print(result) show_result(result) if __name__ == "__main__": main()