From cebc0c464879d5e494cb18c5bb2ffba37979a63f Mon Sep 17 00:00:00 2001 From: Spider Date: Sat, 16 May 2026 19:29:55 +0200 Subject: [PATCH] Dodatak --- mail_checker.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 201 insertions(+), 7 deletions(-) diff --git a/mail_checker.py b/mail_checker.py index eaa064e..f72b498 100644 --- a/mail_checker.py +++ b/mail_checker.py @@ -799,6 +799,7 @@ def _fetch_messages(acc: dict, limit: int = 5) -> list[dict]: is_unread = msg_id in unseen_ids messages.append({ + "msg_id": msg_id.decode() if isinstance(msg_id, bytes) else str(msg_id), "from": sender, "subject": subject, "date": date_str, @@ -819,6 +820,154 @@ def _fetch_messages(acc: dict, limit: int = 5) -> list[dict]: return messages +def _fetch_body(acc: dict, msg_id: str) -> str: + """Dohvaća tijelo poruke (plain text). Označava poruku kao pročitanu.""" + server = acc["IMAP_SERVER"] + password = _get_password(acc) + mail_conn = None + try: + mail_conn = imaplib.IMAP4_SSL(server, timeout=15) + mail_conn.login(acc["MAIL_USER"], password) + mail_conn.select("inbox") # bez readonly — da može označiti kao pročitano + status, data = mail_conn.fetch(msg_id.encode(), "(RFC822)") + if status != "OK" or not data or not data[0]: + return "(Nije moguće dohvatiti sadržaj poruke)" + raw = data[0][1] + msg = email.message_from_bytes(raw) + body = "" + if msg.is_multipart(): + for part in msg.walk(): + ct = part.get_content_type() + cd = str(part.get("Content-Disposition", "")) + if ct == "text/plain" and "attachment" not in cd: + charset = part.get_content_charset() or "utf-8" + try: + body = part.get_payload(decode=True).decode(charset, errors="replace") + except Exception: + body = part.get_payload(decode=True).decode("utf-8", errors="replace") + break + if not body: + # Fallback na HTML ako nema plain text + for part in msg.walk(): + if part.get_content_type() == "text/html": + charset = part.get_content_charset() or "utf-8" + html = part.get_payload(decode=True).decode(charset, errors="replace") + import re + body = re.sub(r"<[^>]+>", "", html) + break + else: + charset = msg.get_content_charset() or "utf-8" + try: + body = msg.get_payload(decode=True).decode(charset, errors="replace") + except Exception: + body = str(msg.get_payload()) + return body.strip() or "(Prazan sadržaj)" + except Exception as exc: + logger.error("Greška pri dohvatu tijela poruke: %s", exc) + return f"(Greška: {exc})" + finally: + if mail_conn: + try: + mail_conn.logout() + except Exception: + pass + + +def _delete_message(acc: dict, msg_id: str) -> bool: + """Premješta poruku u Trash (označava \Deleted i expunge).""" + server = acc["IMAP_SERVER"] + password = _get_password(acc) + mail_conn = None + try: + mail_conn = imaplib.IMAP4_SSL(server, timeout=15) + mail_conn.login(acc["MAIL_USER"], password) + mail_conn.select("inbox") + # Pokušaj premjestiti u Trash (Gmail / standardni IMAP) + trash_folders = ["[Gmail]/Trash", "Trash", "INBOX.Trash", "Deleted Items", "Deleted Messages"] + moved = False + for folder in trash_folders: + try: + result = mail_conn.copy(msg_id.encode(), folder) + if result[0] == "OK": + moved = True + break + except Exception: + continue + # Označi kao obrisano i expunge + mail_conn.store(msg_id.encode(), "+FLAGS", "\Deleted") + mail_conn.expunge() + return True + except Exception as exc: + logger.error("Greška pri brisanju poruke: %s", exc) + return False + finally: + if mail_conn: + try: + mail_conn.logout() + except Exception: + pass + + +def _open_read_window(acc: dict, msg: dict, on_deleted=None): + """Otvara prozor za čitanje poruke.""" + read_win = tk.Toplevel() + read_win.title(f"Poruka — {msg['subject']}") + read_win.geometry("700x500") + read_win.resizable(True, True) + + # Zaglavlje + hdr = tk.Frame(read_win, bd=1, relief="groove") + hdr.pack(fill="x", padx=10, pady=(10, 4)) + tk.Label(hdr, text=f"Od: {msg['from']}", anchor="w").pack(fill="x", padx=6, pady=1) + tk.Label(hdr, text=f"Naslov: {msg['subject']}", anchor="w").pack(fill="x", padx=6, pady=1) + tk.Label(hdr, text=f"Datum: {msg['date']}", anchor="w").pack(fill="x", padx=6, pady=(1, 4)) + + # Tijelo — Text widget s scrollbarom + body_frame = tk.Frame(read_win) + body_frame.pack(fill="both", expand=True, padx=10, pady=4) + txt = tk.Text(body_frame, wrap="word", state="disabled", font=("TkDefaultFont", 10)) + vsb = ttk.Scrollbar(body_frame, command=txt.yview) + txt.configure(yscrollcommand=vsb.set) + txt.pack(side="left", fill="both", expand=True) + vsb.pack(side="right", fill="y") + + lbl_status = tk.Label(read_win, text="Dohvaćanje sadržaja...", fg="gray") + lbl_status.pack(pady=4) + + # Gumb za brisanje + def _do_delete(): + if not messagebox.askyesno("Potvrda", "Obriši ovu poruku?", parent=read_win): + return + lbl_status.config(text="Brisanje...", fg="gray") + def _bg(): + ok = _delete_message(acc, msg["msg_id"]) + def _ui(): + if ok: + lbl_status.config(text="Poruka obrisana.", fg="green") + read_win.after(800, read_win.destroy) + if on_deleted: + on_deleted() + else: + lbl_status.config(text="Greška pri brisanju!", fg="red") + read_win.after(0, _ui) + threading.Thread(target=_bg, daemon=True).start() + + tk.Button(read_win, text="🗑 Obriši poruku", fg="red", command=_do_delete).pack(pady=(0, 8)) + + # Dohvati tijelo u backgroundu + def _load_body(): + body = _fetch_body(acc, msg["msg_id"]) + def _show(): + txt.configure(state="normal") + txt.delete("1.0", "end") + txt.insert("end", body) + txt.configure(state="disabled") + lbl_status.config(text="") + read_win.after(0, _show) + + threading.Thread(target=_load_body, daemon=True).start() + + # Singleton referenca na prozor poruka _messages_win: list = [None] @@ -878,7 +1027,7 @@ def open_messages_window(icon=None, item=None): y = (sh - wh) // 2 win.geometry(f"+{x}+{y}") - def _build_tab(name, messages): + def _build_tab(name, messages, acc_ref): """Kreira jedan tab — poziva se iz glavnog threada.""" frame = tk.Frame(notebook) notebook.add(frame, text=f" {name} ") @@ -897,19 +1046,64 @@ def open_messages_window(icon=None, item=None): vsb = ttk.Scrollbar(frame, orient="vertical", command=tree.yview) tree.configure(yscrollcommand=vsb.set) - tree.pack(side="left", fill="both", expand=True) - vsb.pack(side="right", fill="y") + + # Donji frame s gumbima + action_frame = tk.Frame(frame) + action_frame.pack(side="bottom", fill="x", pady=4) + + # Pohrani poruke po iid za brzi lookup + msg_map = {} if messages: for m in messages: marker = "●" if m["unread"] else "" tags = ("unread",) if m["unread"] else () - tree.insert("", "end", + iid = tree.insert("", "end", values=(marker, m["from"], m["subject"], m["date"]), tags=tags) + msg_map[iid] = m else: tree.insert("", "end", values=("", "—", "Nema poruka", "—")) + tree.pack(side="left", fill="both", expand=True) + vsb.pack(side="right", fill="y") + + def _get_selected_msg(): + sel = tree.selection() + if not sel: + return None + return msg_map.get(sel[0]) + + def _on_double_click(event): + m = _get_selected_msg() + if m and "msg_id" in m: + _open_read_window(acc_ref, m, + on_deleted=lambda: threading.Thread(target=_do_refresh, daemon=True).start()) + + def _on_delete_btn(): + m = _get_selected_msg() + if not m or "msg_id" not in m: + messagebox.showwarning("Upozorenje", "Odaberi poruku za brisanje.", parent=win) + return + if not messagebox.askyesno("Potvrda", "Obriši poruku: {m['subject']}?", parent=win): + return + def _bg(): + ok = _delete_message(acc_ref, m["msg_id"]) + if ok: + win.after(0, lambda: threading.Thread( + target=_do_refresh, daemon=True).start()) + else: + win.after(0, lambda: messagebox.showerror( + "Greška", "Brisanje nije uspjelo.", parent=win)) + threading.Thread(target=_bg, daemon=True).start() + + tree.bind("", _on_double_click) + + tk.Button(action_frame, text="📖 Čitaj poruku", width=16, + command=lambda: _on_double_click(None)).pack(side="left", padx=8) + tk.Button(action_frame, text="🗑 Obriši", width=12, fg="red", + command=_on_delete_btn).pack(side="left", padx=4) + def _do_refresh(): """Background thread: dohvati podatke pa ažuriraj GUI kroz win.after.""" with _state_lock: @@ -922,7 +1116,7 @@ def open_messages_window(icon=None, item=None): name = acc.get("NICKNAME") or acc["MAIL_USER"].split("@")[0] limit = acc.get("PREVIEW_COUNT", 5) msgs = _fetch_messages(acc, limit=limit) - results.append((name, msgs)) + results.append((name, msgs, acc)) logger.info("Dohvaćeno %d poruka za %s", len(msgs), name) def _finish_ui(): @@ -930,8 +1124,8 @@ def open_messages_window(icon=None, item=None): for tab in notebook.tabs(): notebook.forget(tab) # Dodaj nove - for name, msgs in results: - _build_tab(name, msgs) + for name, msgs, acc_ref in results: + _build_tab(name, msgs, acc_ref) lbl_loading.config(text=f"Zadnje osvježavanje: {time.strftime('%H:%M:%S')}", fg="gray") win.after(0, _finish_ui)