|
|
|
|
@ -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)
|
|
|
|
|
|