feat(correos): interfaz estilo Gmail con detalle, volver y responder

- Vista de detalle separada: al abrir un correo la lista desaparece
  y se muestra solo el contenido con cabecera (asunto, de, fecha)
- Botón "← Volver" para regresar a la bandeja
- Botón "↩ Responder" que pre-rellena destinatario (extraído del From),
  asunto con "Re: ..." y cuerpo con cita del mensaje original

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Olivia Mestre Llobell 2026-02-23 19:24:42 +01:00
parent 84148e6c0e
commit 2d27cda79f
1 changed files with 141 additions and 82 deletions

View File

@ -3,6 +3,7 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import threading import threading
from email.utils import parseaddr
from logica.correo import CorreoClient from logica.correo import CorreoClient
from vista.config import * from vista.config import *
@ -10,7 +11,8 @@ from vista.config import *
class CorreosPanel(ttk.Frame): class CorreosPanel(ttk.Frame):
""" """
Panel de la pestaña Correos. Panel de la pestaña Correos.
Gestión de correos electrónicos con login, bandeja de entrada y redacción. Gestión de correos electrónicos con login, bandeja de entrada,
detalle de correo y redacción/respuesta.
""" """
def __init__(self, parent_notebook, root, *args, **kwargs): def __init__(self, parent_notebook, root, *args, **kwargs):
@ -20,17 +22,22 @@ class CorreosPanel(ttk.Frame):
self.correos_cache = [] self.correos_cache = []
self._autorefresh_id = None self._autorefresh_id = None
self._refreshing = False self._refreshing = False
self._correo_actual = None # Metadatos del correo abierto en detalle
self._detalle_cuerpo = "" # Cuerpo cargado (para responder)
# Frames apilados (login, bandeja, redactar) # Frames apilados (login, bandeja, detalle, redactar)
self.frame_login = ttk.Frame(self) self.frame_login = ttk.Frame(self)
self.frame_bandeja = ttk.Frame(self) self.frame_bandeja = ttk.Frame(self)
self.frame_detalle = ttk.Frame(self)
self.frame_redactar = ttk.Frame(self) self.frame_redactar = ttk.Frame(self)
for f in (self.frame_login, self.frame_bandeja, self.frame_redactar): for f in (self.frame_login, self.frame_bandeja,
self.frame_detalle, self.frame_redactar):
f.place(relx=0, rely=0, relwidth=1, relheight=1) f.place(relx=0, rely=0, relwidth=1, relheight=1)
self._crear_vista_login() self._crear_vista_login()
self._crear_vista_bandeja() self._crear_vista_bandeja()
self._crear_vista_detalle()
self._crear_vista_redactar() self._crear_vista_redactar()
# Precargar config si existe # Precargar config si existe
@ -43,7 +50,6 @@ class CorreosPanel(ttk.Frame):
# VISTA 1: LOGIN # VISTA 1: LOGIN
# ================================================================== # ==================================================================
# Mapeo de nombres visibles a claves internas y puertos por defecto
_CIFRADO_MAP = { _CIFRADO_MAP = {
"Sin cifrar": "none", "Sin cifrar": "none",
"SSL/TLS": "ssl", "SSL/TLS": "ssl",
@ -60,13 +66,11 @@ class CorreosPanel(ttk.Frame):
ttk.Label(contenedor, text="Iniciar Sesion - Correo", ttk.Label(contenedor, text="Iniciar Sesion - Correo",
font=FUENTE_TITULO).grid(row=0, column=0, columnspan=2, pady=(0, 20)) font=FUENTE_TITULO).grid(row=0, column=0, columnspan=2, pady=(0, 20))
# Servidor
ttk.Label(contenedor, text="Servidor:", font=FUENTE_NEGOCIOS).grid( ttk.Label(contenedor, text="Servidor:", font=FUENTE_NEGOCIOS).grid(
row=1, column=0, sticky="e", padx=(0, 10), pady=5) row=1, column=0, sticky="e", padx=(0, 10), pady=5)
self.entry_server = ttk.Entry(contenedor, width=30) self.entry_server = ttk.Entry(contenedor, width=30)
self.entry_server.grid(row=1, column=1, pady=5) self.entry_server.grid(row=1, column=1, pady=5)
# Cifrado (3 opciones predefinidas + Personalizado)
ttk.Label(contenedor, text="Cifrado:", font=FUENTE_NEGOCIOS).grid( ttk.Label(contenedor, text="Cifrado:", font=FUENTE_NEGOCIOS).grid(
row=2, column=0, sticky="e", padx=(0, 10), pady=5) row=2, column=0, sticky="e", padx=(0, 10), pady=5)
self.var_cifrado = tk.StringVar(value="Sin cifrar") self.var_cifrado = tk.StringVar(value="Sin cifrar")
@ -76,62 +80,51 @@ class CorreosPanel(ttk.Frame):
combo_cifrado.grid(row=2, column=1, pady=5) combo_cifrado.grid(row=2, column=1, pady=5)
combo_cifrado.bind("<<ComboboxSelected>>", self._on_cifrado_change) combo_cifrado.bind("<<ComboboxSelected>>", self._on_cifrado_change)
# Puerto IMAP (oculto por defecto)
self.lbl_imap_port = ttk.Label(contenedor, text="Puerto IMAP:", font=FUENTE_NEGOCIOS) self.lbl_imap_port = ttk.Label(contenedor, text="Puerto IMAP:", font=FUENTE_NEGOCIOS)
self.entry_imap_port = ttk.Entry(contenedor, width=30) self.entry_imap_port = ttk.Entry(contenedor, width=30)
self.entry_imap_port.insert(0, "143") self.entry_imap_port.insert(0, "143")
# Puerto SMTP (oculto por defecto)
self.lbl_smtp_port = ttk.Label(contenedor, text="Puerto SMTP:", font=FUENTE_NEGOCIOS) self.lbl_smtp_port = ttk.Label(contenedor, text="Puerto SMTP:", font=FUENTE_NEGOCIOS)
self.entry_smtp_port = ttk.Entry(contenedor, width=30) self.entry_smtp_port = ttk.Entry(contenedor, width=30)
self.entry_smtp_port.insert(0, "25") self.entry_smtp_port.insert(0, "25")
# Usuario
ttk.Label(contenedor, text="Usuario:", font=FUENTE_NEGOCIOS).grid( ttk.Label(contenedor, text="Usuario:", font=FUENTE_NEGOCIOS).grid(
row=5, column=0, sticky="e", padx=(0, 10), pady=5) row=5, column=0, sticky="e", padx=(0, 10), pady=5)
self.entry_user = ttk.Entry(contenedor, width=30) self.entry_user = ttk.Entry(contenedor, width=30)
self.entry_user.grid(row=5, column=1, pady=5) self.entry_user.grid(row=5, column=1, pady=5)
# Contraseña
ttk.Label(contenedor, text="Contraseña:", font=FUENTE_NEGOCIOS).grid( ttk.Label(contenedor, text="Contraseña:", font=FUENTE_NEGOCIOS).grid(
row=6, column=0, sticky="e", padx=(0, 10), pady=5) row=6, column=0, sticky="e", padx=(0, 10), pady=5)
self.entry_password = ttk.Entry(contenedor, width=30, show="*") self.entry_password = ttk.Entry(contenedor, width=30, show="*")
self.entry_password.grid(row=6, column=1, pady=5) self.entry_password.grid(row=6, column=1, pady=5)
# Recordar credenciales
self.var_recordar = tk.BooleanVar(value=False) self.var_recordar = tk.BooleanVar(value=False)
ttk.Checkbutton(contenedor, text="Recordar credenciales", ttk.Checkbutton(contenedor, text="Recordar credenciales",
variable=self.var_recordar).grid( variable=self.var_recordar).grid(
row=7, column=0, columnspan=2, pady=10) row=7, column=0, columnspan=2, pady=10)
# Botón conectar
self.btn_conectar = ttk.Button(contenedor, text="Conectar", self.btn_conectar = ttk.Button(contenedor, text="Conectar",
command=self._on_conectar) command=self._on_conectar)
self.btn_conectar.grid(row=8, column=0, columnspan=2, pady=10) self.btn_conectar.grid(row=8, column=0, columnspan=2, pady=10)
# Label de estado
self.lbl_estado_login = ttk.Label(contenedor, text="", font=FUENTE_NOTA, self.lbl_estado_login = ttk.Label(contenedor, text="", font=FUENTE_NOTA,
foreground=COLOR_ACCION) foreground=COLOR_ACCION)
self.lbl_estado_login.grid(row=9, column=0, columnspan=2) self.lbl_estado_login.grid(row=9, column=0, columnspan=2)
def _on_cifrado_change(self, event=None): def _on_cifrado_change(self, event=None):
"""Muestra/oculta puertos y actualiza valores por defecto."""
from logica.correo import CorreoClient from logica.correo import CorreoClient
opcion = self.var_cifrado.get() opcion = self.var_cifrado.get()
if opcion == "Personalizado": if opcion == "Personalizado":
# Mostrar campos de puertos editables
self.lbl_imap_port.grid(row=3, column=0, sticky="e", padx=(0, 10), pady=5) self.lbl_imap_port.grid(row=3, column=0, sticky="e", padx=(0, 10), pady=5)
self.entry_imap_port.grid(row=3, column=1, pady=5) self.entry_imap_port.grid(row=3, column=1, pady=5)
self.lbl_smtp_port.grid(row=4, column=0, sticky="e", padx=(0, 10), pady=5) self.lbl_smtp_port.grid(row=4, column=0, sticky="e", padx=(0, 10), pady=5)
self.entry_smtp_port.grid(row=4, column=1, pady=5) self.entry_smtp_port.grid(row=4, column=1, pady=5)
else: else:
# Ocultar campos de puertos
self.lbl_imap_port.grid_remove() self.lbl_imap_port.grid_remove()
self.entry_imap_port.grid_remove() self.entry_imap_port.grid_remove()
self.lbl_smtp_port.grid_remove() self.lbl_smtp_port.grid_remove()
self.entry_smtp_port.grid_remove() self.entry_smtp_port.grid_remove()
# Establecer puertos por defecto según la opción
sec_key = self._CIFRADO_MAP.get(opcion, "none") sec_key = self._CIFRADO_MAP.get(opcion, "none")
puertos = CorreoClient.PUERTOS.get(sec_key, CorreoClient.PUERTOS["none"]) puertos = CorreoClient.PUERTOS.get(sec_key, CorreoClient.PUERTOS["none"])
self.entry_imap_port.delete(0, tk.END) self.entry_imap_port.delete(0, tk.END)
@ -140,7 +133,6 @@ class CorreosPanel(ttk.Frame):
self.entry_smtp_port.insert(0, str(puertos["smtp"])) self.entry_smtp_port.insert(0, str(puertos["smtp"]))
def _precargar_config(self): def _precargar_config(self):
"""Precarga credenciales desde .env si existen."""
config = self.cliente.cargar_config() config = self.cliente.cargar_config()
if config["server"]: if config["server"]:
self.entry_server.insert(0, config["server"]) self.entry_server.insert(0, config["server"])
@ -150,7 +142,6 @@ class CorreosPanel(ttk.Frame):
self.entry_smtp_port.insert(0, config["smtp_port"]) self.entry_smtp_port.insert(0, config["smtp_port"])
self.entry_user.insert(0, config["user"]) self.entry_user.insert(0, config["user"])
self.entry_password.insert(0, config["password"]) self.entry_password.insert(0, config["password"])
# Restaurar modo de cifrado
sec = config["security"] sec = config["security"]
nombre = self._CIFRADO_INVERSO.get(sec, "Sin cifrar") nombre = self._CIFRADO_INVERSO.get(sec, "Sin cifrar")
self.var_cifrado.set(nombre) self.var_cifrado.set(nombre)
@ -158,14 +149,12 @@ class CorreosPanel(ttk.Frame):
self.var_recordar.set(True) self.var_recordar.set(True)
def _get_security(self): def _get_security(self):
"""Devuelve la clave de seguridad interna según la selección del usuario."""
opcion = self.var_cifrado.get() opcion = self.var_cifrado.get()
if opcion == "Personalizado": if opcion == "Personalizado":
return "none" # Personalizado usa puertos manuales sin cifrado extra return "none"
return self._CIFRADO_MAP.get(opcion, "none") return self._CIFRADO_MAP.get(opcion, "none")
def _on_conectar(self): def _on_conectar(self):
"""Intenta conectar al servidor IMAP."""
server = self.entry_server.get().strip() server = self.entry_server.get().strip()
imap_port = self.entry_imap_port.get().strip() imap_port = self.entry_imap_port.get().strip()
smtp_port = self.entry_smtp_port.get().strip() smtp_port = self.entry_smtp_port.get().strip()
@ -178,14 +167,12 @@ class CorreosPanel(ttk.Frame):
foreground="red") foreground="red")
return return
# Guardar si se pidió
if self.var_recordar.get(): if self.var_recordar.get():
self.cliente.guardar_config(server, imap_port, smtp_port, user, password, security) self.cliente.guardar_config(server, imap_port, smtp_port, user, password, security)
self.lbl_estado_login.config(text="Conectando...", foreground=COLOR_ACCION) self.lbl_estado_login.config(text="Conectando...", foreground=COLOR_ACCION)
self.btn_conectar.config(state="disabled") self.btn_conectar.config(state="disabled")
# Almacenar datos SMTP para uso posterior
self.smtp_server = server self.smtp_server = server
self.smtp_port = smtp_port self.smtp_port = smtp_port
self.smtp_user = user self.smtp_user = user
@ -232,12 +219,12 @@ class CorreosPanel(ttk.Frame):
ttk.Button(barra, text="Cerrar Sesión", ttk.Button(barra, text="Cerrar Sesión",
command=self._on_logout).pack(side="right", padx=(5, 0)) command=self._on_logout).pack(side="right", padx=(5, 0))
ttk.Button(barra, text="Redactar", ttk.Button(barra, text="Redactar",
command=self._on_redactar).pack(side="right", padx=(5, 0)) command=self._on_redactar).pack(side="right", padx=(5, 0))
ttk.Button(barra, text="Actualizar", ttk.Button(barra, text="Actualizar",
command=self._on_actualizar).pack(side="right", padx=(5, 0)) command=self._on_actualizar).pack(side="right", padx=(5, 0))
# Treeview bandeja # Treeview bandeja (ocupa todo el espacio restante)
tree_frame = ttk.Frame(frame) tree_frame = ttk.Frame(frame)
tree_frame.pack(fill="both", expand=True, padx=10, pady=5) tree_frame.pack(fill="both", expand=True, padx=10, pady=5)
@ -261,16 +248,7 @@ class CorreosPanel(ttk.Frame):
self.tree.bind("<Double-1>", self._on_doble_clic_correo) self.tree.bind("<Double-1>", self._on_doble_clic_correo)
# Área de lectura de correo
lbl_detalle = ttk.Label(frame, text="Contenido del correo:", font=FUENTE_NEGOCIOS)
lbl_detalle.pack(anchor="w", padx=10, pady=(5, 0))
self.txt_lectura = tk.Text(frame, height=10, wrap="word", font=FUENTE_MONO,
state="disabled", bg=COLOR_FONDO)
self.txt_lectura.pack(fill="both", expand=True, padx=10, pady=(0, 10))
def _poblar_bandeja(self, correos): def _poblar_bandeja(self, correos):
"""Rellena el Treeview con la lista de correos preservando la selección."""
self.correos_cache = correos self.correos_cache = correos
sel_actual = self.tree.selection() sel_actual = self.tree.selection()
uid_sel = sel_actual[0] if sel_actual else None uid_sel = sel_actual[0] if sel_actual else None
@ -287,7 +265,6 @@ class CorreosPanel(ttk.Frame):
self.tree.see(uid_sel) self.tree.see(uid_sel)
def _on_doble_clic_correo(self, event): def _on_doble_clic_correo(self, event):
"""Muestra el contenido del correo seleccionado y lo marca como leído."""
sel = self.tree.selection() sel = self.tree.selection()
if not sel: if not sel:
return return
@ -295,34 +272,41 @@ class CorreosPanel(ttk.Frame):
# Marcar visualmente como leído de inmediato # Marcar visualmente como leído de inmediato
self.tree.item(uid, tags=("leido",)) self.tree.item(uid, tags=("leido",))
correo_meta = None
for c in self.correos_cache: for c in self.correos_cache:
if c["uid"] == uid: if c["uid"] == uid:
c["leido"] = True c["leido"] = True
correo_meta = c
break break
self.txt_lectura.config(state="normal") self._correo_actual = correo_meta or {"uid": uid, "de": "", "asunto": "", "fecha": ""}
self.txt_lectura.delete("1.0", "end") self._detalle_cuerpo = ""
self.txt_lectura.insert("1.0", "Cargando...")
self.txt_lectura.config(state="disabled") # Rellenar cabeceras del frame detalle
self.lbl_det_asunto.config(text=self._correo_actual.get("asunto", "(Sin asunto)"))
self.lbl_det_de.config(text=f"De: {self._correo_actual.get('de', '')}")
self.lbl_det_fecha.config(text=f"Fecha: {self._correo_actual.get('fecha', '')}")
# Mostrar "Cargando..." y navegar al detalle
self.txt_detalle.config(state="normal")
self.txt_detalle.delete("1.0", "end")
self.txt_detalle.insert("1.0", "Cargando...")
self.txt_detalle.config(state="disabled")
self.btn_responder.config(state="disabled")
self._mostrar_frame(self.frame_detalle)
def tarea(): def tarea():
try: try:
contenido = self.cliente.leer_correo(uid) contenido = self.cliente.leer_correo(uid)
self.cliente.marcar_leido(uid) self.cliente.marcar_leido(uid)
self.root.after(0, lambda: self._mostrar_contenido(contenido)) self.root.after(0, lambda: self._mostrar_detalle(contenido))
except Exception as e: except Exception as e:
self.root.after(0, lambda: self._mostrar_contenido(f"Error: {e}")) self.root.after(0, lambda: self._mostrar_detalle(f"Error: {e}"))
threading.Thread(target=tarea, daemon=True).start() threading.Thread(target=tarea, daemon=True).start()
def _mostrar_contenido(self, texto):
self.txt_lectura.config(state="normal")
self.txt_lectura.delete("1.0", "end")
self.txt_lectura.insert("1.0", texto)
self.txt_lectura.config(state="disabled")
def _on_actualizar(self): def _on_actualizar(self):
"""Refresca la bandeja de entrada."""
def tarea(): def tarea():
try: try:
correos = self.cliente.obtener_bandeja() correos = self.cliente.obtener_bandeja()
@ -333,17 +317,12 @@ class CorreosPanel(ttk.Frame):
threading.Thread(target=tarea, daemon=True).start() threading.Thread(target=tarea, daemon=True).start()
def _on_logout(self): def _on_logout(self):
"""Cierra sesión y vuelve al login."""
self._detener_autorefresh() self._detener_autorefresh()
self.cliente.desconectar() self.cliente.desconectar()
self.tree.delete(*self.tree.get_children()) self.tree.delete(*self.tree.get_children())
self.txt_lectura.config(state="normal")
self.txt_lectura.delete("1.0", "end")
self.txt_lectura.config(state="disabled")
self._mostrar_frame(self.frame_login) self._mostrar_frame(self.frame_login)
def _on_redactar(self): def _on_redactar(self):
"""Muestra la vista de redacción."""
self.entry_para.delete(0, tk.END) self.entry_para.delete(0, tk.END)
self.entry_asunto_red.delete(0, tk.END) self.entry_asunto_red.delete(0, tk.END)
self.txt_cuerpo.delete("1.0", "end") self.txt_cuerpo.delete("1.0", "end")
@ -351,7 +330,97 @@ class CorreosPanel(ttk.Frame):
self._mostrar_frame(self.frame_redactar) self._mostrar_frame(self.frame_redactar)
# ================================================================== # ==================================================================
# VISTA 3: REDACTAR CORREO # VISTA 3: DETALLE DE CORREO
# ==================================================================
def _crear_vista_detalle(self):
frame = self.frame_detalle
# Barra de acciones
barra = ttk.Frame(frame)
barra.pack(fill="x", padx=10, pady=(10, 5))
ttk.Button(barra, text="← Volver",
command=self._on_volver_bandeja).pack(side="left")
self.btn_responder = ttk.Button(barra, text="↩ Responder",
command=self._on_responder, state="disabled")
self.btn_responder.pack(side="left", padx=(10, 0))
# Cabecera del correo
info = ttk.Frame(frame, padding=(12, 8))
info.pack(fill="x")
self.lbl_det_asunto = ttk.Label(info, text="",
font=(FUENTE_FAMILIA, 13, "bold"),
wraplength=700, justify="left")
self.lbl_det_asunto.pack(anchor="w")
self.lbl_det_de = ttk.Label(info, text="", font=FUENTE_NEGOCIOS,
foreground=COLOR_TEXTO)
self.lbl_det_de.pack(anchor="w", pady=(6, 0))
self.lbl_det_fecha = ttk.Label(info, text="", font=FUENTE_NOTA,
foreground="#888888")
self.lbl_det_fecha.pack(anchor="w")
ttk.Separator(frame, orient="horizontal").pack(fill="x", padx=10, pady=5)
# Cuerpo del correo
cuerpo_frame = ttk.Frame(frame)
cuerpo_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
self.txt_detalle = tk.Text(cuerpo_frame, wrap="word", font=FUENTE_MONO,
state="disabled", bg=COLOR_FONDO, relief="flat",
padx=8, pady=8)
scroll_det = ttk.Scrollbar(cuerpo_frame, orient="vertical",
command=self.txt_detalle.yview)
self.txt_detalle.configure(yscrollcommand=scroll_det.set)
scroll_det.pack(side="right", fill="y")
self.txt_detalle.pack(side="left", fill="both", expand=True)
def _mostrar_detalle(self, texto):
self._detalle_cuerpo = texto
self.txt_detalle.config(state="normal")
self.txt_detalle.delete("1.0", "end")
self.txt_detalle.insert("1.0", texto)
self.txt_detalle.config(state="disabled")
self.btn_responder.config(state="normal")
def _on_volver_bandeja(self):
self._mostrar_frame(self.frame_bandeja)
def _on_responder(self):
if not self._correo_actual:
return
# Extraer dirección de correo del remitente
_, addr = parseaddr(self._correo_actual.get("de", ""))
para = addr or self._correo_actual.get("de", "")
# Asunto con "Re:" si no lo tiene ya
asunto = self._correo_actual.get("asunto", "")
if not asunto.lower().startswith("re:"):
asunto = f"Re: {asunto}"
# Cuerpo con cita del mensaje original
cita = "\n\n--- Mensaje original ---\n" + "\n".join(
f"> {linea}" for linea in self._detalle_cuerpo.splitlines()
)
self.entry_para.delete(0, tk.END)
self.entry_para.insert(0, para)
self.entry_asunto_red.delete(0, tk.END)
self.entry_asunto_red.insert(0, asunto)
self.txt_cuerpo.delete("1.0", "end")
self.txt_cuerpo.insert("1.0", cita)
self.txt_cuerpo.mark_set("insert", "1.0")
self.lbl_estado_envio.config(text="")
self._mostrar_frame(self.frame_redactar)
# ==================================================================
# VISTA 4: REDACTAR CORREO
# ================================================================== # ==================================================================
def _crear_vista_redactar(self): def _crear_vista_redactar(self):
@ -363,27 +432,23 @@ class CorreosPanel(ttk.Frame):
ttk.Label(contenedor, text="Redactar Correo", font=FUENTE_TITULO).pack( ttk.Label(contenedor, text="Redactar Correo", font=FUENTE_TITULO).pack(
anchor="w", pady=(0, 15)) anchor="w", pady=(0, 15))
# Para
fila_para = ttk.Frame(contenedor) fila_para = ttk.Frame(contenedor)
fila_para.pack(fill="x", pady=3) fila_para.pack(fill="x", pady=3)
ttk.Label(fila_para, text="Para:", font=FUENTE_NEGOCIOS, width=10).pack(side="left") ttk.Label(fila_para, text="Para:", font=FUENTE_NEGOCIOS, width=10).pack(side="left")
self.entry_para = ttk.Entry(fila_para) self.entry_para = ttk.Entry(fila_para)
self.entry_para.pack(side="left", fill="x", expand=True) self.entry_para.pack(side="left", fill="x", expand=True)
# Asunto
fila_asunto = ttk.Frame(contenedor) fila_asunto = ttk.Frame(contenedor)
fila_asunto.pack(fill="x", pady=3) fila_asunto.pack(fill="x", pady=3)
ttk.Label(fila_asunto, text="Asunto:", font=FUENTE_NEGOCIOS, width=10).pack(side="left") ttk.Label(fila_asunto, text="Asunto:", font=FUENTE_NEGOCIOS, width=10).pack(side="left")
self.entry_asunto_red = ttk.Entry(fila_asunto) self.entry_asunto_red = ttk.Entry(fila_asunto)
self.entry_asunto_red.pack(side="left", fill="x", expand=True) self.entry_asunto_red.pack(side="left", fill="x", expand=True)
# Cuerpo
ttk.Label(contenedor, text="Mensaje:", font=FUENTE_NEGOCIOS).pack( ttk.Label(contenedor, text="Mensaje:", font=FUENTE_NEGOCIOS).pack(
anchor="w", pady=(10, 3)) anchor="w", pady=(10, 3))
self.txt_cuerpo = tk.Text(contenedor, wrap="word", font=FUENTE_MONO, height=12) self.txt_cuerpo = tk.Text(contenedor, wrap="word", font=FUENTE_MONO, height=12)
self.txt_cuerpo.pack(fill="both", expand=True) self.txt_cuerpo.pack(fill="both", expand=True)
# Botones
fila_btns = ttk.Frame(contenedor) fila_btns = ttk.Frame(contenedor)
fila_btns.pack(fill="x", pady=(10, 0)) fila_btns.pack(fill="x", pady=(10, 0))
@ -396,7 +461,6 @@ class CorreosPanel(ttk.Frame):
self.lbl_estado_envio.pack(side="left", padx=15) self.lbl_estado_envio.pack(side="left", padx=15)
def _on_enviar(self): def _on_enviar(self):
"""Envía el correo redactado."""
to = self.entry_para.get().strip() to = self.entry_para.get().strip()
subject = self.entry_asunto_red.get().strip() subject = self.entry_asunto_red.get().strip()
body = self.txt_cuerpo.get("1.0", "end").strip() body = self.txt_cuerpo.get("1.0", "end").strip()
@ -446,7 +510,6 @@ class CorreosPanel(ttk.Frame):
) )
def _autorefresh_tick(self): def _autorefresh_tick(self):
"""Lanzado desde el hilo principal. Si ya hay refresco en curso, reintenta en el siguiente ciclo."""
if self._refreshing: if self._refreshing:
self._autorefresh_id = self.root.after( self._autorefresh_id = self.root.after(
self._INTERVALO_REFRESCO_MS, self._autorefresh_tick self._INTERVALO_REFRESCO_MS, self._autorefresh_tick
@ -465,11 +528,9 @@ class CorreosPanel(ttk.Frame):
threading.Thread(target=tarea, daemon=True).start() threading.Thread(target=tarea, daemon=True).start()
def _fin_autorefresh(self, correos): def _fin_autorefresh(self, correos):
"""Siempre se ejecuta en el hilo principal."""
self._refreshing = False self._refreshing = False
if correos is not None: if correos is not None:
self._poblar_bandeja(correos) self._poblar_bandeja(correos)
# Programar siguiente tick desde el hilo principal
self._autorefresh_id = self.root.after( self._autorefresh_id = self.root.after(
self._INTERVALO_REFRESCO_MS, self._autorefresh_tick self._INTERVALO_REFRESCO_MS, self._autorefresh_tick
) )
@ -485,10 +546,8 @@ class CorreosPanel(ttk.Frame):
# ================================================================== # ==================================================================
def cerrar(self): def cerrar(self):
"""Limpieza al cerrar la aplicación: para el auto-refresco y desconecta IMAP."""
self._detener_autorefresh() self._detener_autorefresh()
self.cliente.desconectar() self.cliente.desconectar()
def _mostrar_frame(self, frame): def _mostrar_frame(self, frame):
"""Levanta el frame indicado al frente."""
frame.tkraise() frame.tkraise()