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:
parent
84148e6c0e
commit
2d27cda79f
|
|
@ -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,37 +149,33 @@ 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()
|
||||||
user = self.entry_user.get().strip()
|
user = self.entry_user.get().strip()
|
||||||
password = self.entry_password.get().strip()
|
password = self.entry_password.get().strip()
|
||||||
security = self._get_security()
|
security = self._get_security()
|
||||||
|
|
||||||
if not server or not user or not password:
|
if not server or not user or not password:
|
||||||
self.lbl_estado_login.config(text="Rellena todos los campos obligatorios.",
|
self.lbl_estado_login.config(text="Rellena todos los campos obligatorios.",
|
||||||
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
|
|
||||||
self.smtp_password = password
|
self.smtp_password = password
|
||||||
self.smtp_security = security
|
self.smtp_security = security
|
||||||
|
|
||||||
|
|
@ -232,23 +219,23 @@ 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)
|
||||||
|
|
||||||
cols = ("de", "asunto", "fecha")
|
cols = ("de", "asunto", "fecha")
|
||||||
self.tree = ttk.Treeview(tree_frame, columns=cols, show="headings", selectmode="browse")
|
self.tree = ttk.Treeview(tree_frame, columns=cols, show="headings", selectmode="browse")
|
||||||
self.tree.heading("de", text="De")
|
self.tree.heading("de", text="De")
|
||||||
self.tree.heading("asunto", text="Asunto")
|
self.tree.heading("asunto", text="Asunto")
|
||||||
self.tree.heading("fecha", text="Fecha")
|
self.tree.heading("fecha", text="Fecha")
|
||||||
self.tree.column("de", width=200, minwidth=120)
|
self.tree.column("de", width=200, minwidth=120)
|
||||||
self.tree.column("asunto", width=300, minwidth=150)
|
self.tree.column("asunto", width=300, minwidth=150)
|
||||||
self.tree.column("fecha", width=180, minwidth=100)
|
self.tree.column("fecha", width=180, minwidth=100)
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
|
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
|
||||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
@ -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,10 +461,9 @@ 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()
|
||||||
|
|
||||||
if not to or not subject:
|
if not to or not subject:
|
||||||
self.lbl_estado_envio.config(text="Destinatario y asunto son obligatorios.",
|
self.lbl_estado_envio.config(text="Destinatario y asunto son obligatorios.",
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue