From 5719219f5867bbd7e094362c50e790df6297cbad Mon Sep 17 00:00:00 2001 From: Levi Planelles Date: Mon, 2 Mar 2026 19:35:01 +0100 Subject: [PATCH] first commit --- INSTALACION.md | 111 ++++++ README.md | 163 +++++++++ cliente_correo.py | 879 ++++++++++++++++++++++++++++++++++++++++++++++ ejecutar.sh | 25 ++ 4 files changed, 1178 insertions(+) create mode 100644 INSTALACION.md create mode 100644 README.md create mode 100644 cliente_correo.py create mode 100755 ejecutar.sh diff --git a/INSTALACION.md b/INSTALACION.md new file mode 100644 index 0000000..dfa85ac --- /dev/null +++ b/INSTALACION.md @@ -0,0 +1,111 @@ +# Guía de Instalación - Cliente de Correo + +## Problema: Error de Tkinter en macOS + +Si recibes el error `"macOS 26 (2602) or later required, have instead 16 (1602) !"`, significa que tu versión de Python está usando una versión antigua de Tcl/Tk. + +## Solución: Instalar Python desde python.org + +### Opción 1: Instalar Python 3.11 o superior (RECOMENDADO) + +1. **Descargar Python**: + - Ve a https://www.python.org/downloads/ + - Descarga Python 3.11 o superior para macOS + - Busca el instalador `.pkg` para macOS + +2. **Instalar**: + - Abre el archivo `.pkg` descargado + - Sigue el asistente de instalación + - Acepta todas las opciones por defecto + +3. **Verificar instalación**: + ```bash + python3 --version + ``` + Deberías ver algo como: `Python 3.11.x` o superior + +4. **Ejecutar el cliente**: + ```bash + python3 cliente_correo.py + ``` + +### Opción 2: Usar Homebrew (Alternativa) + +Si tienes Homebrew instalado: + +```bash +# Instalar Python con Homebrew +brew install python@3.11 + +# Verificar +python3 --version + +# Ejecutar el cliente +python3 cliente_correo.py +``` + +### Opción 3: Usar pyenv (Para desarrolladores) + +```bash +# Instalar pyenv +brew install pyenv + +# Instalar Python 3.11 +pyenv install 3.11.7 + +# Establecer como versión local +pyenv local 3.11.7 + +# Ejecutar el cliente +python cliente_correo.py +``` + +## Verificar que Tkinter funciona + +Después de instalar Python actualizado, verifica que Tkinter funcione: + +```bash +python3 -m tkinter +``` + +Esto debería abrir una ventana de prueba de Tkinter. Si ves la ventana, todo está bien. + +## Notas Importantes + +- **NO uses el Python que viene con macOS** (`/usr/bin/python`), ya que suele estar desactualizado +- Después de instalar Python desde python.org, usa siempre `python3` en lugar de `python` +- Si ya tienes múltiples versiones de Python, puedes verificar cuál estás usando con: + ```bash + which python3 + ``` + Debería mostrar algo como `/Library/Frameworks/Python.framework/...` + +## Si aún tienes problemas + +### Verificar la versión de Tcl/Tk: + +```bash +python3 -c "import tkinter; print(tkinter.TkVersion)" +``` + +Debería mostrar `8.6` o superior. + +### Reinstalar Python: + +Si instalaste Python pero aún tienes problemas: + +1. Desinstala Python antiguo +2. Descarga la última versión de python.org +3. Instala de nuevo +4. Abre una nueva terminal (importante) +5. Intenta ejecutar el cliente de nuevo + +## Alternativas sin instalar Python + +Si no puedes o no quieres instalar Python actualizado, puedo crear versiones alternativas: + +1. **Cliente web**: Interfaz HTML que funciona en el navegador +2. **Cliente CLI**: Interfaz de línea de comandos sin GUI +3. **Cliente con PyQt5**: Requiere `pip install PyQt5` + +Házmelo saber si necesitas alguna de estas alternativas. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1384c5 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# Cliente de Correo Electrónico + +Cliente de correo electrónico con interfaz gráfica desarrollado en Python con soporte para SMTP, IMAP y POP3. + +## Características + +- **Envío de correos** mediante SMTP (puerto 25) + - **Enviar archivos adjuntos** de cualquier tipo + - Múltiples archivos adjuntos por correo +- **Consulta de correos** mediante IMAP (puerto 143) +- **Descarga de correos** mediante POP3 (puerto 110) +- **Inicio de sesión automático** con credenciales predefinidas (levi@psp.es) +- **Soporte completo para archivos adjuntos**: + - Visualizar lista de adjuntos en correos recibidos + - **Previsualizar adjuntos** (imágenes y archivos de texto) + - Guardar adjuntos individuales o todos a la vez +- Interfaz gráfica intuitiva con Tkinter +- Soporte para múltiples codificaciones de caracteres +- Decodificación automática de encabezados + +## Requisitos + +- Python 3.12 o superior (recomendado) +- Bibliotecas de Python: + - tkinter (incluido con Python) + - Pillow (para previsualización de imágenes) + - smtplib, imaplib, poplib, email (incluidos) + +## Configuración del Servidor + +El cliente está configurado por defecto para conectarse a: + +- **IP del servidor**: 10.10.0.101 +- **Puerto SMTP**: 25 +- **Puerto IMAP**: 143 +- **Puerto POP3**: 110 +- **Web**: https://10.10.0.101:20000 + +## Instalación + +1. Clona o descarga este repositorio +2. No se requieren dependencias adicionales + +## Uso + +### Ejecución + +```bash +python cliente_correo.py +``` + +O en sistemas Unix/Linux: + +```bash +python3 cliente_correo.py +``` + +### Inicio de Sesión + +El cliente inicia sesión automáticamente con las credenciales predefinidas: +- **Usuario**: levi@psp.es +- **Contraseña**: 1234 + +Si necesitas usar otras credenciales, puedes modificarlas en la pestaña "Iniciar Sesión" y hacer clic en "Conectar". + +### Enviar Correos (SMTP) + +1. Ve a la pestaña "Enviar Correo" +2. Completa los campos: + - **Para**: Dirección de correo del destinatario + - **Asunto**: Asunto del correo +3. **(Opcional) Adjuntar archivos**: + - Haz clic en "Agregar Archivo" en la sección "Adjuntar Archivos" + - Selecciona uno o varios archivos + - Los archivos aparecerán en la lista + - Puedes quitar archivos seleccionándolos y haciendo clic en "Quitar Seleccionado" +4. Escribe tu mensaje en el campo "Mensaje" +5. Haz clic en "Enviar Correo" + +### Consultar Correos (IMAP) + +1. Ve a la pestaña "Bandeja de Entrada (IMAP)" +2. Haz clic en "Actualizar" para cargar los correos +3. Selecciona un correo de la lista +4. Haz clic en "Leer Seleccionado" para ver su contenido + +#### Trabajar con Adjuntos Recibidos + +Si el correo tiene archivos adjuntos: +1. Los adjuntos aparecerán listados en la sección "Archivos Adjuntos" +2. Para **previsualizar** un adjunto: + - Selecciona el adjunto de la lista + - Haz clic en "Previsualizar" + - Se abrirá una ventana mostrando el contenido (funciona con imágenes y archivos de texto) +3. Para **guardar** un adjunto: + - Selecciona el adjunto de la lista + - Haz clic en "Guardar Adjunto" + - Elige la ubicación donde guardarlo +3. Para guardar todos los adjuntos: + - Haz clic en "Guardar Todos" + - Selecciona la carpeta de destino + +### Descargar Correos (POP3) + +1. Ve a la pestaña "Descargar Correos (POP3)" +2. Haz clic en "Listar Correos" para ver los correos disponibles +3. Selecciona un correo de la lista o ingresa su número +4. Haz clic en "Descargar Seleccionado" para ver su contenido + +## Estructura del Proyecto + +``` +correo/ +├── cliente_correo.py # Archivo principal con toda la funcionalidad +└── README.md # Este archivo +``` + +## Funcionalidades Técnicas + +### Clase ClienteCorreo + +- **crear_interfaz()**: Inicializa la interfaz gráfica con pestañas +- **conectar()**: Establece conexión con el servidor IMAP +- **enviar_correo()**: Envía correos mediante SMTP +- **cargar_correos_imap()**: Lista correos en la bandeja de entrada +- **leer_correo_imap()**: Lee un correo específico mediante IMAP +- **listar_correos_pop()**: Lista correos disponibles mediante POP3 +- **descargar_correo_pop()**: Descarga y muestra un correo mediante POP3 +- **decodificar_header()**: Decodifica encabezados de correos +- **obtener_cuerpo_email()**: Extrae el cuerpo del mensaje + +## Seguridad + +- Las contraseñas se manejan en memoria y no se almacenan +- Conexiones sin cifrado SSL/TLS (configuración típica de servidores internos) +- Para entornos de producción, se recomienda usar SSL/TLS + +## Solución de Problemas + +### Error de conexión + +- Verifica que el servidor esté accesible en la IP 10.10.0.101 +- Asegúrate de que los puertos no estén bloqueados por un firewall +- Confirma que las credenciales sean correctas + +### No se pueden cargar los correos + +- Verifica que hayas iniciado sesión correctamente +- Asegúrate de tener correos en tu bandeja de entrada +- Revisa que el servidor IMAP/POP3 esté funcionando + +### Caracteres extraños en los correos + +- El cliente intenta decodificar automáticamente varios encodings +- Si persisten problemas, verifica la codificación del correo original + +## Licencia + +Este proyecto es de código abierto y está disponible para uso educativo. + +## Autor + +Proyecto desarrollado para PSP (Programación de Servicios y Procesos) diff --git a/cliente_correo.py b/cliente_correo.py new file mode 100644 index 0000000..ce76244 --- /dev/null +++ b/cliente_correo.py @@ -0,0 +1,879 @@ +#!/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() diff --git a/ejecutar.sh b/ejecutar.sh new file mode 100755 index 0000000..bec532b --- /dev/null +++ b/ejecutar.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Script para ejecutar el cliente de correo con Python 3.12 +# Esto asegura que se use la versión correcta de Python con Tkinter actualizado + +echo "===================================" +echo "Cliente de Correo Electrónico" +echo "===================================" +echo "" + +# Verificar si Python 3.12 está instalado +if [ -f "/opt/homebrew/bin/python3.12" ]; then + echo "✓ Python 3.12 encontrado" + /opt/homebrew/bin/python3.12 cliente_correo.py +elif command -v python3.12 &> /dev/null; then + echo "✓ Python 3.12 encontrado en PATH" + python3.12 cliente_correo.py +else + echo "✗ Error: Python 3.12 no está instalado" + echo "" + echo "Para instalar Python 3.12 ejecuta:" + echo " brew install python@3.12 python-tk@3.12" + echo "" + exit 1 +fi