main
Spider 2 months ago
parent 3031b95f03
commit cebc0c4648

@ -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("<Double-1>", _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)

Loading…
Cancel
Save

Powered by TurnKey Linux.