correo/cliente_correo.py

880 lines
36 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Cliente de Correo Electrónico
Soporta SMTP, IMAP y POP3
"""
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, filedialog, Toplevel, Text
import smtplib
import imaplib
import poplib
import email
import email.utils
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from email.header import decode_header
import ssl
from datetime import datetime
import re
import os
import mimetypes
from PIL import Image, ImageTk
import io
class ClienteCorreo:
def __init__(self, root):
self.root = root
self.root.title("Cliente de Correo Electrónico")
self.root.geometry("1000x700")
# Configuración del servidor
self.servidor = "10.10.0.101"
self.puerto_smtp = 25
self.puerto_imap = 143
self.puerto_pop = 110
# Credenciales predefinidas
self.usuario_predefinido = "levi@psp.es"
self.password_predefinida = "1234"
# Variables de sesión
self.usuario = None
self.password = None
self.conexion_imap = None
# Lista de adjuntos del correo actual
self.adjuntos_actuales = []
# Crear interfaz
self.crear_interfaz()
# Iniciar sesión automáticamente (comentado temporalmente si el servidor no está disponible)
# Descomenta la siguiente línea cuando el servidor esté activo:
# self.root.after(100, self.auto_login)
def crear_interfaz(self):
"""Crea la interfaz gráfica principal"""
# Notebook para pestañas
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Pestaña de Login (única visible al inicio)
self.tab_login = ttk.Frame(self.notebook)
self.notebook.add(self.tab_login, text="Iniciar Sesión")
self.crear_tab_login()
# Crear las otras pestañas pero NO agregarlas al notebook todavía
# Pestaña de Enviar
self.tab_enviar = ttk.Frame(self.notebook)
self.crear_tab_enviar()
# Pestaña de Bandeja (IMAP)
self.tab_bandeja = ttk.Frame(self.notebook)
self.crear_tab_bandeja()
# Pestaña de Descargar (POP3)
self.tab_pop = ttk.Frame(self.notebook)
self.crear_tab_pop()
# Barra de estado
self.status_bar = tk.Label(self.root, text="Desconectado", bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def crear_tab_login(self):
"""Crea la pestaña de inicio de sesión"""
frame = ttk.Frame(self.tab_login, padding="20")
frame.pack(expand=True)
ttk.Label(frame, text="Cliente de Correo Electrónico", font=("Arial", 16, "bold")).grid(row=0, column=0, columnspan=2, pady=20)
ttk.Label(frame, text="Servidor:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.entry_servidor = ttk.Entry(frame, width=30)
self.entry_servidor.insert(0, self.servidor)
self.entry_servidor.grid(row=1, column=1, pady=5)
ttk.Label(frame, text="Usuario (email):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.entry_usuario = ttk.Entry(frame, width=30)
self.entry_usuario.insert(0, self.usuario_predefinido)
self.entry_usuario.grid(row=2, column=1, pady=5)
ttk.Label(frame, text="Contraseña:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.entry_password = ttk.Entry(frame, width=30, show="*")
self.entry_password.insert(0, self.password_predefinida)
self.entry_password.grid(row=3, column=1, pady=5)
ttk.Button(frame, text="Conectar", command=self.conectar).grid(row=4, column=0, columnspan=2, pady=20)
# Información de conexión
info_frame = ttk.LabelFrame(frame, text="Información del Servidor", padding="10")
info_frame.grid(row=5, column=0, columnspan=2, pady=10, sticky=tk.EW)
ttk.Label(info_frame, text=f"SMTP: {self.puerto_smtp}").pack(anchor=tk.W)
ttk.Label(info_frame, text=f"IMAP: {self.puerto_imap}").pack(anchor=tk.W)
ttk.Label(info_frame, text=f"POP3: {self.puerto_pop}").pack(anchor=tk.W)
def crear_tab_enviar(self):
"""Crea la pestaña de envío de correos"""
frame = ttk.Frame(self.tab_enviar, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Destinatario
ttk.Label(frame, text="Para:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.entry_para = ttk.Entry(frame, width=60)
self.entry_para.grid(row=0, column=1, sticky=tk.EW, pady=5, padx=5)
# Asunto
ttk.Label(frame, text="Asunto:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.entry_asunto = ttk.Entry(frame, width=60)
self.entry_asunto.grid(row=1, column=1, sticky=tk.EW, pady=5, padx=5)
# Archivos adjuntos para enviar
adjuntos_enviar_frame = ttk.LabelFrame(frame, text="Adjuntar Archivos", padding="5")
adjuntos_enviar_frame.grid(row=2, column=0, columnspan=2, sticky=tk.EW, pady=5, padx=5)
self.lista_adjuntos_enviar = tk.Listbox(adjuntos_enviar_frame, height=3)
self.lista_adjuntos_enviar.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
btn_frame = ttk.Frame(adjuntos_enviar_frame)
btn_frame.pack(side=tk.RIGHT, padx=5)
ttk.Button(btn_frame, text="Agregar Archivo", command=self.agregar_archivo_adjunto).pack(pady=2)
ttk.Button(btn_frame, text="Quitar Seleccionado", command=self.quitar_archivo_adjunto).pack(pady=2)
# Lista interna de archivos adjuntos
self.archivos_adjuntos_enviar = []
# Mensaje
ttk.Label(frame, text="Mensaje:").grid(row=3, column=0, sticky=tk.NW, pady=5)
self.text_mensaje = scrolledtext.ScrolledText(frame, width=60, height=15)
self.text_mensaje.grid(row=3, column=1, sticky=tk.NSEW, pady=5, padx=5)
# Botón enviar
ttk.Button(frame, text="Enviar Correo", command=self.enviar_correo).grid(row=4, column=1, pady=10, sticky=tk.E)
# Configurar expansión
frame.columnconfigure(1, weight=1)
frame.rowconfigure(3, weight=1)
def crear_tab_bandeja(self):
"""Crea la pestaña de bandeja de entrada (IMAP)"""
frame = ttk.Frame(self.tab_bandeja, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Controles
control_frame = ttk.Frame(frame)
control_frame.pack(fill=tk.X, pady=5)
ttk.Button(control_frame, text="Actualizar", command=self.cargar_correos_imap).pack(side=tk.LEFT, padx=5)
# Frame para adjuntos
self.adjuntos_frame = ttk.LabelFrame(frame, text="Archivos Adjuntos", padding="5")
self.adjuntos_frame.pack(fill=tk.X, pady=5)
self.adjuntos_listbox = tk.Listbox(self.adjuntos_frame, height=3)
self.adjuntos_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
# Bind para previsualizar al hacer clic
self.adjuntos_listbox.bind('<<ListboxSelect>>', lambda e: self.previsualizar_adjunto())
# Crear menú contextual para descargar con clic derecho
self.menu_adjuntos = tk.Menu(self.adjuntos_listbox, tearoff=0)
self.menu_adjuntos.add_command(label="Descargar archivo", command=self.guardar_adjunto)
self.menu_adjuntos.add_separator()
self.menu_adjuntos.add_command(label="Descargar todos", command=self.guardar_todos_adjuntos)
# Bind clic derecho para mostrar menú contextual
self.adjuntos_listbox.bind("<Button-2>", self.mostrar_menu_adjuntos) # Mac: botón derecho
self.adjuntos_listbox.bind("<Button-3>", self.mostrar_menu_adjuntos) # Windows/Linux: botón derecho
adjuntos_btn_frame = ttk.Frame(self.adjuntos_frame)
adjuntos_btn_frame.pack(side=tk.RIGHT, padx=5)
ttk.Button(adjuntos_btn_frame, text="Guardar Todos", command=self.guardar_todos_adjuntos).pack(pady=2)
# Lista de correos
list_frame = ttk.Frame(frame)
list_frame.pack(fill=tk.BOTH, expand=True, pady=5)
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.lista_correos = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, height=10)
self.lista_correos.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.lista_correos.yview)
# Bind para leer correo automáticamente al seleccionar
self.lista_correos.bind('<<ListboxSelect>>', lambda e: self.leer_correo_imap())
# Área de lectura
ttk.Label(frame, text="Contenido del correo:").pack(anchor=tk.W, pady=5)
self.text_lectura = scrolledtext.ScrolledText(frame, width=80, height=15)
self.text_lectura.pack(fill=tk.BOTH, expand=True, pady=5)
def crear_tab_pop(self):
"""Crea la pestaña de descarga de correos (POP3)"""
frame = ttk.Frame(self.tab_pop, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Controles
control_frame = ttk.Frame(frame)
control_frame.pack(fill=tk.X, pady=5)
ttk.Button(control_frame, text="Listar Correos", command=self.listar_correos_pop).pack(side=tk.LEFT, padx=5)
# Lista de correos
list_frame = ttk.Frame(frame)
list_frame.pack(fill=tk.BOTH, expand=True, pady=5)
scrollbar_pop = ttk.Scrollbar(list_frame)
scrollbar_pop.pack(side=tk.RIGHT, fill=tk.Y)
self.lista_correos_pop = tk.Listbox(list_frame, yscrollcommand=scrollbar_pop.set, height=10)
self.lista_correos_pop.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar_pop.config(command=self.lista_correos_pop.yview)
# Bind para descargar correo automáticamente al seleccionar
self.lista_correos_pop.bind('<<ListboxSelect>>', lambda e: self.descargar_correo_pop())
# Área de lectura
ttk.Label(frame, text="Contenido del correo:").pack(anchor=tk.W, pady=5)
self.text_lectura_pop = scrolledtext.ScrolledText(frame, width=80, height=15)
self.text_lectura_pop.pack(fill=tk.BOTH, expand=True, pady=5)
def auto_login(self):
"""Inicia sesión automáticamente con las credenciales predefinidas"""
try:
# Probar conexión IMAP
imap = imaplib.IMAP4(self.servidor, self.puerto_imap)
imap.login(self.usuario_predefinido, self.password_predefinida)
imap.logout()
# Guardar credenciales
self.usuario = self.usuario_predefinido
self.password = self.password_predefinida
self.status_bar.config(text=f"Conectado como: {self.usuario}")
# Mostrar las pestañas adicionales
self.mostrar_pestanas()
# Cambiar a la pestaña de bandeja de entrada
self.notebook.select(self.tab_bandeja)
except Exception as e:
self.status_bar.config(text=f"Error de auto-login: {str(e)}")
messagebox.showerror("Error de Conexión", f"No se pudo conectar automáticamente:\n{str(e)}\n\nPor favor, use la pestaña 'Iniciar Sesión'")
def mostrar_pestanas(self):
"""Muestra las pestañas de correo después de login exitoso"""
# Verificar si ya están agregadas
if self.notebook.index("end") == 1: # Solo hay pestaña de login
self.notebook.add(self.tab_enviar, text="Enviar Correo")
self.notebook.add(self.tab_bandeja, text="Bandeja de Entrada (IMAP)")
self.notebook.add(self.tab_pop, text="Descargar Correos (POP3)")
def conectar(self):
"""Conecta al servidor de correo"""
servidor = self.entry_servidor.get()
usuario = self.entry_usuario.get()
password = self.entry_password.get()
if not usuario or not password:
messagebox.showerror("Error", "Por favor ingrese usuario y contraseña")
return
try:
# Probar conexión IMAP
imap = imaplib.IMAP4(servidor, self.puerto_imap)
imap.login(usuario, password)
imap.logout()
# Guardar credenciales
self.servidor = servidor
self.usuario = usuario
self.password = password
self.status_bar.config(text=f"Conectado como: {usuario}")
messagebox.showinfo("Éxito", "Conexión exitosa al servidor de correo")
# Mostrar las pestañas adicionales
self.mostrar_pestanas()
# Cambiar a la pestaña de bandeja de entrada
self.notebook.select(self.tab_bandeja)
except Exception as e:
messagebox.showerror("Error de Conexión", f"No se pudo conectar al servidor:\n{str(e)}")
def enviar_correo(self):
"""Envía un correo electrónico usando SMTP"""
if not self.usuario or not self.password:
messagebox.showerror("Error", "Primero debe iniciar sesión")
return
destinatario = self.entry_para.get()
asunto = self.entry_asunto.get()
mensaje = self.text_mensaje.get("1.0", tk.END)
if not destinatario or not asunto:
messagebox.showerror("Error", "Por favor complete todos los campos")
return
try:
# Crear mensaje
msg = MIMEMultipart()
msg['From'] = self.usuario
msg['To'] = destinatario
msg['Subject'] = asunto
msg['Date'] = email.utils.formatdate(localtime=True)
# Agregar cuerpo del mensaje
msg.attach(MIMEText(mensaje, 'plain'))
# Agregar archivos adjuntos
for filepath in self.archivos_adjuntos_enviar:
try:
with open(filepath, 'rb') as f:
# Determinar el tipo MIME del archivo
mime_type, _ = mimetypes.guess_type(filepath)
if mime_type is None:
mime_type = 'application/octet-stream'
main_type, sub_type = mime_type.split('/', 1)
# Crear adjunto
adjunto = MIMEBase(main_type, sub_type)
adjunto.set_payload(f.read())
encoders.encode_base64(adjunto)
# Agregar encabezado con el nombre del archivo
filename = os.path.basename(filepath)
adjunto.add_header('Content-Disposition', f'attachment; filename="{filename}"')
msg.attach(adjunto)
except Exception as e:
messagebox.showwarning("Advertencia", f"No se pudo adjuntar {os.path.basename(filepath)}: {str(e)}")
# Conectar y enviar
servidor = smtplib.SMTP(self.servidor, self.puerto_smtp)
servidor.set_debuglevel(0)
# Intentar login (puede que no sea necesario según configuración del servidor)
try:
servidor.login(self.usuario, self.password)
except:
pass # Algunos servidores no requieren autenticación en puerto 25
servidor.send_message(msg)
servidor.quit()
messagebox.showinfo("Éxito", "Correo enviado exitosamente")
# Limpiar campos
self.entry_para.delete(0, tk.END)
self.entry_asunto.delete(0, tk.END)
self.text_mensaje.delete("1.0", tk.END)
# Limpiar lista de adjuntos
self.archivos_adjuntos_enviar.clear()
self.lista_adjuntos_enviar.delete(0, tk.END)
except Exception as e:
messagebox.showerror("Error", f"No se pudo enviar el correo:\n{str(e)}")
def cargar_correos_imap(self):
"""Carga la lista de correos usando IMAP"""
if not self.usuario or not self.password:
messagebox.showerror("Error", "Primero debe iniciar sesión")
return
try:
# Conectar a IMAP
self.conexion_imap = imaplib.IMAP4(self.servidor, self.puerto_imap)
self.conexion_imap.login(self.usuario, self.password)
self.conexion_imap.select('INBOX')
# Buscar todos los correos *
status, messages = self.conexion_imap.search(None, 'ALL')
if status != 'OK':
messagebox.showerror("Error", "No se pudieron obtener los correos")
return
# Limpiar lista
self.lista_correos.delete(0, tk.END)
# Obtener IDs de mensajes
message_ids = messages[0].split()
if not message_ids:
self.lista_correos.insert(tk.END, "No hay correos en la bandeja")
return
# Mostrar últimos 20 correos (más recientes primero)
for msg_id in reversed(message_ids[-20:]):
status, msg_data = self.conexion_imap.fetch(msg_id, '(RFC822.HEADER)')
if status == 'OK' and msg_data and msg_data[0]:
email_message = email.message_from_bytes(msg_data[0][1]) # type: ignore
subject = self.decodificar_header(email_message['Subject'])
from_addr = self.decodificar_header(email_message['From'])
date = email_message['Date']
# Formato: ID - De - Asunto
item = f"[{msg_id.decode()}] De: {from_addr} - {subject}"
self.lista_correos.insert(tk.END, item)
messagebox.showinfo("Éxito", f"Se encontraron {len(message_ids)} correos")
except Exception as e:
messagebox.showerror("Error", f"No se pudieron cargar los correos:\n{str(e)}")
def leer_correo_imap(self):
"""Lee el correo seleccionado de la lista IMAP"""
seleccion = self.lista_correos.curselection()
if not seleccion:
return # No hay selección, no hacer nada
try:
# Extraer ID del correo
item_text = self.lista_correos.get(seleccion[0])
match = re.search(r'\[(\d+)\]', item_text)
if not match:
messagebox.showerror("Error", "No se pudo obtener el ID del correo")
return
msg_id = match.group(1)
# Obtener el correo completo
if not self.conexion_imap:
self.conexion_imap = imaplib.IMAP4(self.servidor, self.puerto_imap)
if self.usuario and self.password:
self.conexion_imap.login(self.usuario, self.password)
self.conexion_imap.select('INBOX')
status, msg_data = self.conexion_imap.fetch(msg_id, '(RFC822)')
if status != 'OK':
messagebox.showerror("Error", "No se pudo obtener el correo")
return
# Parsear el mensaje
if msg_data and msg_data[0]:
email_message = email.message_from_bytes(msg_data[0][1]) # type: ignore
else:
messagebox.showerror("Error", "No se pudo parsear el mensaje")
return
# Limpiar área de lectura
self.text_lectura.delete("1.0", tk.END)
# Limpiar lista de adjuntos
self.adjuntos_listbox.delete(0, tk.END)
self.adjuntos_actuales = []
# Mostrar encabezados
self.text_lectura.insert(tk.END, f"De: {self.decodificar_header(email_message['From'])}\n")
self.text_lectura.insert(tk.END, f"Para: {self.decodificar_header(email_message['To'])}\n")
self.text_lectura.insert(tk.END, f"Asunto: {self.decodificar_header(email_message['Subject'])}\n")
self.text_lectura.insert(tk.END, f"Fecha: {email_message['Date']}\n")
# Extraer y mostrar adjuntos
adjuntos_info = self.extraer_adjuntos(email_message)
if adjuntos_info:
self.text_lectura.insert(tk.END, f"\nAdjuntos: {len(adjuntos_info)} archivo(s)\n")
for nombre, _ in adjuntos_info:
self.adjuntos_listbox.insert(tk.END, nombre)
self.text_lectura.insert(tk.END, "\n" + "="*80 + "\n\n")
# Mostrar cuerpo del mensaje
cuerpo = self.obtener_cuerpo_email(email_message)
self.text_lectura.insert(tk.END, cuerpo)
except Exception as e:
messagebox.showerror("Error", f"No se pudo leer el correo:\n{str(e)}")
def listar_correos_pop(self):
"""Lista los correos usando POP3"""
if not self.usuario or not self.password:
messagebox.showerror("Error", "Primero debe iniciar sesión")
return
try:
# Conectar a POP3
pop = poplib.POP3(self.servidor, self.puerto_pop)
pop.user(self.usuario)
pop.pass_(self.password)
# Obtener número de mensajes
num_messages = len(pop.list()[1])
# Limpiar lista
self.lista_correos_pop.delete(0, tk.END)
if num_messages == 0:
self.lista_correos_pop.insert(tk.END, "No hay correos en el servidor")
pop.quit()
return
# Listar los últimos 20 correos
inicio = max(1, num_messages - 19)
for i in range(num_messages, inicio - 1, -1):
# Obtener solo los encabezados
response, headers, octets = pop.top(i, 0)
email_message = email.message_from_bytes(b'\n'.join(headers))
subject = self.decodificar_header(email_message.get('Subject', 'Sin asunto'))
from_addr = self.decodificar_header(email_message.get('From', 'Desconocido'))
item = f"[{i}] De: {from_addr} - {subject}"
self.lista_correos_pop.insert(tk.END, item)
pop.quit()
messagebox.showinfo("Éxito", f"Se encontraron {num_messages} correos en el servidor")
except Exception as e:
messagebox.showerror("Error", f"No se pudieron listar los correos:\n{str(e)}")
def descargar_correo_pop(self):
"""Descarga y muestra un correo usando POP3"""
# Obtener de la selección
seleccion = self.lista_correos_pop.curselection()
if not seleccion:
return # No hay selección, no hacer nada
item_text = self.lista_correos_pop.get(seleccion[0])
match = re.search(r'\[(\d+)\]', item_text)
if not match:
return
num_correo = match.group(1)
if not self.usuario or not self.password:
messagebox.showerror("Error", "Primero debe iniciar sesión")
return
try:
# Conectar a POP3
pop = poplib.POP3(self.servidor, self.puerto_pop)
pop.user(self.usuario)
pop.pass_(self.password)
# Descargar el correo
response, lines, octets = pop.retr(int(num_correo))
# Parsear el mensaje
msg_content = b'\n'.join(lines)
email_message = email.message_from_bytes(msg_content)
# Limpiar área de lectura
self.text_lectura_pop.delete("1.0", tk.END)
# Mostrar encabezados
self.text_lectura_pop.insert(tk.END, f"De: {self.decodificar_header(email_message['From'])}\n")
self.text_lectura_pop.insert(tk.END, f"Para: {self.decodificar_header(email_message['To'])}\n")
self.text_lectura_pop.insert(tk.END, f"Asunto: {self.decodificar_header(email_message['Subject'])}\n")
self.text_lectura_pop.insert(tk.END, f"Fecha: {email_message['Date']}\n")
self.text_lectura_pop.insert(tk.END, "\n" + "="*80 + "\n\n")
# Mostrar cuerpo del mensaje
cuerpo = self.obtener_cuerpo_email(email_message)
self.text_lectura_pop.insert(tk.END, cuerpo)
pop.quit()
except Exception as e:
messagebox.showerror("Error", f"No se pudo descargar el correo:\n{str(e)}")
def decodificar_header(self, header):
"""Decodifica los encabezados de los correos"""
if header is None:
return ""
decoded_parts = []
for part, encoding in decode_header(header):
if isinstance(part, bytes):
if encoding:
try:
decoded_parts.append(part.decode(encoding))
except:
decoded_parts.append(part.decode('utf-8', errors='ignore'))
else:
decoded_parts.append(part.decode('utf-8', errors='ignore'))
else:
decoded_parts.append(str(part))
return ''.join(decoded_parts)
def obtener_cuerpo_email(self, email_message):
"""Extrae el cuerpo del mensaje de email"""
cuerpo = ""
if email_message.is_multipart():
for part in email_message.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
if content_type == "text/plain" and "attachment" not in content_disposition:
try:
payload = part.get_payload(decode=True)
charset = part.get_content_charset() or 'utf-8'
cuerpo += payload.decode(charset, errors='ignore')
except:
cuerpo += str(part.get_payload())
else:
try:
payload = email_message.get_payload(decode=True)
charset = email_message.get_content_charset() or 'utf-8'
if payload:
cuerpo = payload.decode(charset, errors='ignore')
else:
cuerpo = str(email_message.get_payload())
except:
cuerpo = str(email_message.get_payload())
return cuerpo if cuerpo else "No se pudo decodificar el contenido del mensaje"
def extraer_adjuntos(self, email_message):
"""Extrae los adjuntos de un mensaje de email"""
adjuntos = []
if email_message.is_multipart():
for part in email_message.walk():
content_disposition = str(part.get("Content-Disposition", ""))
# Si tiene Content-Disposition y es attachment
if "attachment" in content_disposition:
filename = part.get_filename()
if filename:
# Decodificar el nombre del archivo
filename = self.decodificar_header(filename)
# Obtener el contenido del adjunto
payload = part.get_payload(decode=True)
if payload:
adjuntos.append((filename, payload))
self.adjuntos_actuales.append((filename, payload))
return adjuntos
def mostrar_menu_adjuntos(self, event):
"""Muestra el menú contextual para adjuntos"""
# Seleccionar el item bajo el cursor
index = self.adjuntos_listbox.nearest(event.y)
self.adjuntos_listbox.selection_clear(0, tk.END)
self.adjuntos_listbox.selection_set(index)
self.adjuntos_listbox.activate(index)
# Mostrar menú en la posición del cursor
try:
self.menu_adjuntos.tk_popup(event.x_root, event.y_root)
finally:
self.menu_adjuntos.grab_release()
def guardar_adjunto(self):
"""Guarda el adjunto seleccionado"""
seleccion = self.adjuntos_listbox.curselection()
if not seleccion:
messagebox.showwarning("Advertencia", "Por favor seleccione un adjunto")
return
if not self.adjuntos_actuales:
messagebox.showerror("Error", "No hay adjuntos disponibles")
return
try:
idx = seleccion[0]
filename, payload = self.adjuntos_actuales[idx]
# Pedir al usuario dónde guardar el archivo
filepath = filedialog.asksaveasfilename(
initialfile=filename,
title="Guardar adjunto",
defaultextension="",
filetypes=[("Todos los archivos", "*.*")]
)
if filepath:
with open(filepath, 'wb') as f:
f.write(payload)
messagebox.showinfo("Éxito", f"Adjunto guardado en:\n{filepath}")
except Exception as e:
messagebox.showerror("Error", f"No se pudo guardar el adjunto:\n{str(e)}")
def guardar_todos_adjuntos(self):
"""Guarda todos los adjuntos en una carpeta"""
if not self.adjuntos_actuales:
messagebox.showerror("Error", "No hay adjuntos disponibles")
return
try:
# Pedir al usuario que seleccione una carpeta
carpeta = filedialog.askdirectory(title="Seleccione carpeta para guardar adjuntos")
if carpeta:
guardados = 0
for filename, payload in self.adjuntos_actuales:
filepath = os.path.join(carpeta, filename)
# Si el archivo ya existe, agregar un número
base, ext = os.path.splitext(filepath)
counter = 1
while os.path.exists(filepath):
filepath = f"{base}_{counter}{ext}"
counter += 1
with open(filepath, 'wb') as f:
f.write(payload)
guardados += 1
messagebox.showinfo("Éxito", f"{guardados} adjunto(s) guardado(s) en:\n{carpeta}")
except Exception as e:
messagebox.showerror("Error", f"No se pudieron guardar los adjuntos:\n{str(e)}")
def agregar_archivo_adjunto(self):
"""Agrega un archivo adjunto para enviar"""
try:
filepaths = filedialog.askopenfilenames(
title="Seleccionar archivos para adjuntar",
filetypes=[
("Todos los archivos", "*.*"),
("Imágenes", "*.png *.jpg *.jpeg *.gif *.bmp"),
("Documentos", "*.pdf *.doc *.docx *.txt"),
("Archivos comprimidos", "*.zip *.rar *.7z")
]
)
if filepaths:
for filepath in filepaths:
if filepath not in self.archivos_adjuntos_enviar:
self.archivos_adjuntos_enviar.append(filepath)
filename = os.path.basename(filepath)
self.lista_adjuntos_enviar.insert(tk.END, filename)
except Exception as e:
messagebox.showerror("Error", f"No se pudo agregar el archivo:\n{str(e)}")
def quitar_archivo_adjunto(self):
"""Quita un archivo adjunto de la lista de envío"""
seleccion = self.lista_adjuntos_enviar.curselection()
if not seleccion:
messagebox.showwarning("Advertencia", "Por favor seleccione un archivo")
return
try:
idx = seleccion[0]
self.archivos_adjuntos_enviar.pop(idx)
self.lista_adjuntos_enviar.delete(idx)
except Exception as e:
messagebox.showerror("Error", f"No se pudo quitar el archivo:\n{str(e)}")
def previsualizar_adjunto(self):
"""Previsualiza el adjunto seleccionado"""
seleccion = self.adjuntos_listbox.curselection()
if not seleccion:
return # No hay selección, no hacer nada (llamado automáticamente)
if not self.adjuntos_actuales:
messagebox.showerror("Error", "No hay adjuntos disponibles")
return
try:
idx = seleccion[0]
filename, payload = self.adjuntos_actuales[idx]
# Determinar el tipo de archivo
mime_type, _ = mimetypes.guess_type(filename)
# Crear ventana de previsualización
preview_window = Toplevel(self.root)
preview_window.title(f"Previsualización: {filename}")
preview_window.geometry("800x600")
# Frame con información
info_frame = ttk.Frame(preview_window)
info_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Label(info_frame, text=f"Archivo: {filename}", font=("Arial", 10, "bold")).pack(anchor=tk.W)
ttk.Label(info_frame, text=f"Tamaño: {len(payload)} bytes", font=("Arial", 9)).pack(anchor=tk.W)
if mime_type:
ttk.Label(info_frame, text=f"Tipo: {mime_type}", font=("Arial", 9)).pack(anchor=tk.W)
# Frame para contenido
content_frame = ttk.Frame(preview_window)
content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Intentar previsualizar según el tipo
if mime_type and mime_type.startswith('image/'):
# Previsualización de imagen
try:
image = Image.open(io.BytesIO(payload))
# Redimensionar si es muy grande
max_size = (750, 500)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(image)
label = tk.Label(content_frame, image=photo)
label.image = photo # Mantener referencia
label.pack()
except Exception as e:
ttk.Label(content_frame, text=f"No se pudo mostrar la imagen:\n{str(e)}").pack()
elif mime_type and mime_type.startswith('text/'):
# Previsualización de texto
try:
text_content = payload.decode('utf-8', errors='ignore')
text_widget = scrolledtext.ScrolledText(content_frame, wrap=tk.WORD)
text_widget.pack(fill=tk.BOTH, expand=True)
text_widget.insert('1.0', text_content)
text_widget.config(state='disabled')
except Exception as e:
ttk.Label(content_frame, text=f"No se pudo mostrar el texto:\n{str(e)}").pack()
else:
# Tipo no soportado para previsualización
ttk.Label(content_frame,
text=f"Previsualización no disponible para este tipo de archivo.\n\n"
f"Tipo MIME: {mime_type or 'Desconocido'}\n"
f"Puede guardar el archivo para verlo en su aplicación correspondiente.",
justify=tk.CENTER).pack(expand=True)
# Botón para cerrar
ttk.Button(preview_window, text="Cerrar", command=preview_window.destroy).pack(pady=10)
except Exception as e:
messagebox.showerror("Error", f"No se pudo previsualizar el adjunto:\n{str(e)}")
def main():
root = tk.Tk()
app = ClienteCorreo(root)
root.mainloop()
if __name__ == "__main__":
main()