#!/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('<>', 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("", self.mostrar_menu_adjuntos) # Mac: botón derecho self.adjuntos_listbox.bind("", 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('<>', 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('<>', 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()