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