first commit

This commit is contained in:
Levi Planelles 2026-03-02 19:35:01 +01:00
commit 5719219f58
4 changed files with 1178 additions and 0 deletions

111
INSTALACION.md Normal file
View File

@ -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.

163
README.md Normal file
View File

@ -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)

879
cliente_correo.py Normal file
View File

@ -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('<<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()

25
ejecutar.sh Executable file
View File

@ -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