correos y chat mejorado

This commit is contained in:
Алекс 2026-02-27 18:36:12 +01:00
parent ecaa57fcb1
commit f9c3bc1702
5 changed files with 703 additions and 105 deletions

0
ReadMe.md Normal file → Executable file
View File

Binary file not shown.

603
client.py Normal file → Executable file
View File

@ -34,6 +34,247 @@ def start_client():
# Cola de eventos para comunicación hilo->GUI
event_queue = queue.Queue()
# Función para abrir ventana de correo (T4. Servicios)
def abrir_ventana_correo():
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# --- Sesión en memoria ---
email_session = {'server': None, 'user': None, 'password': None, 'main_win': None}
def logout():
if email_session['main_win']:
email_session['main_win'].destroy()
email_session['main_win'] = None
email_session['server'] = None
email_session['user'] = None
email_session['password'] = None
abrir_ventana_correo() # Volver a pedir login
def show_login():
login_win = tk.Toplevel(root)
login_win.title("Login Email")
login_win.geometry("350x250")
login_win.resizable(False, False)
login_win.configure(bg="#f5f5f5")
tk.Label(login_win, text="Acceso Email", font=('Helvetica', 15, 'bold'), bg="#f5f5f5").pack(pady=12)
frm = tk.Frame(login_win, bg="#f5f5f5")
frm.pack(pady=10)
tk.Label(frm, text="Servidor:", bg="#f5f5f5").grid(row=0, column=0, sticky="e", pady=4)
entry_server = tk.Entry(frm, width=22)
entry_server.insert(0, email_session['server'] or "10.10.0.101")
entry_server.grid(row=0, column=1, pady=4)
tk.Label(frm, text="Usuario:", bg="#f5f5f5").grid(row=1, column=0, sticky="e", pady=4)
entry_user = tk.Entry(frm, width=22)
if email_session['user']:
entry_user.insert(0, email_session['user'])
entry_user.grid(row=1, column=1, pady=4)
tk.Label(frm, text="Contraseña:", bg="#f5f5f5").grid(row=2, column=0, sticky="e", pady=4)
entry_pass = tk.Entry(frm, width=22, show="*")
if email_session['password']:
entry_pass.insert(0, email_session['password'])
entry_pass.grid(row=2, column=1, pady=4)
status = tk.Label(login_win, text="", bg="#f5f5f5", fg="red")
status.pack(pady=4)
def intentar_login():
import imaplib
server = entry_server.get().strip()
user = entry_user.get().strip()
password = entry_pass.get()
if not all([server, user, password]):
status.config(text="Completa todos los campos")
return
try:
mail = imaplib.IMAP4(server, 143)
mail.login(user, password)
mail.logout()
login_win.destroy()
email_session['server'] = server
email_session['user'] = user
email_session['password'] = password
mostrar_inbox_y_envio()
except Exception as e:
status.config(text=f"Error: {e}")
tk.Button(login_win, text="Entrar", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=intentar_login).pack(pady=10)
def mostrar_inbox_y_envio():
win = tk.Toplevel(root)
win.title(f"Correo - {email_session['user']}")
win.geometry("900x600")
win.configure(bg="#f5f5f5")
email_session['main_win'] = win
top = tk.Frame(win, bg="#f5f5f5")
top.pack(fill="x")
tk.Label(top, text=f"Usuario: {email_session['user']}", font=('Arial', 12, 'bold'), bg="#f5f5f5").pack(side="left", padx=10, pady=8)
tk.Button(top, text="Logout", bg="#ffcccc", command=logout).pack(side="right", padx=10, pady=8)
btns = tk.Frame(win, bg="#f5f5f5")
btns.pack(pady=4)
tk.Button(btns, text="📥 Ver INBOX", bg="#ddeeff", font=('Arial', 11), width=14, command=lambda: ver_correos_recibidos(email_session['server'], email_session['user'], email_session['password'], parent=win)).pack(side="left", padx=10)
tk.Button(btns, text="📤 Enviar", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=lambda: abrir_envio_correo(email_session['server'], email_session['user'], email_session['password'])).pack(side="left", padx=10)
# Mostrar INBOX directamente
ver_correos_recibidos(email_session['server'], email_session['user'], email_session['password'], parent=win)
# Iniciar login o sesión
if not email_session['user']:
show_login()
else:
mostrar_inbox_y_envio()
# (Eliminada definición duplicada de ver_correos_recibidos)
def ver_correos_recibidos(server, usuario, contrasena, parent=None):
import imaplib, email, datetime
from email.utils import parsedate_to_datetime
imap_port = 143
# Si parent es None, crear ventana nueva, si no, usar parent
if parent is None:
win = tk.Toplevel(root)
else:
# Limpiar parent
for widget in parent.winfo_children():
if isinstance(widget, ttk.Treeview) or isinstance(widget, tk.Label):
widget.destroy()
win = parent
win.title(f"INBOX de {usuario}")
win.configure(bg="#f5f5f5")
tk.Label(win, text=f"INBOX de {usuario}", font=('Arial', 14, 'bold'), bg="#f5f5f5").pack(pady=8)
columns = ("De", "Asunto", "Fecha", "Acción")
tree = ttk.Treeview(win, columns=columns, show="headings", height=18)
for col, w in zip(columns, (200, 320, 120, 80)):
tree.heading(col, text=col)
tree.column(col, width=w, anchor="w")
tree.pack(padx=10, pady=8, fill="both", expand=True)
status = tk.Label(win, text="Cargando...", fg="gray", bg="#f5f5f5")
status.pack(pady=4)
correos = []
def cargar():
try:
mail = imaplib.IMAP4(server, imap_port)
mail.login(usuario, contrasena)
mail.select('INBOX')
typ, data = mail.search(None, 'ALL')
ids = data[0].split()
if not ids:
status.config(text="No hay correos.")
return
for num in reversed(ids):
typ, msg_data = mail.fetch(num, '(RFC822)')
for response_part in msg_data:
if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part[1])
asunto = email.header.decode_header(msg.get('Subject'))[0][0]
if isinstance(asunto, bytes):
asunto = asunto.decode(errors='ignore')
de = msg.get('From')
fecha_raw = msg.get('Date')
fecha = fecha_raw or ''
try:
if fecha_raw:
fecha_dt = parsedate_to_datetime(fecha_raw)
fecha = fecha_dt.strftime('%d/%m/%Y %H:%M')
except Exception:
pass
correos.append((num, de, asunto, fecha, msg))
tree.insert('', 'end', values=(de, asunto, fecha, 'Ver/Responder'))
status.config(text=f"Mostrando {len(ids)} correos.")
mail.logout()
except Exception as e:
status.config(text=f"Error: {e}", fg="red")
threading.Thread(target=cargar, daemon=True).start()
def on_select(event):
item = tree.selection()
if not item:
return
idx = tree.index(item[0])
num, de, asunto, fecha, msg = correos[idx]
detalle = tk.Toplevel(win)
detalle.title(f"Correo de {de}")
detalle.geometry("600x500")
detalle.configure(bg="#f5f5f5")
tk.Label(detalle, text=f"De: {de}", font=('Arial', 11, 'bold'), bg="#f5f5f5").pack(anchor="w", padx=12, pady=4)
tk.Label(detalle, text=f"Asunto: {asunto}", font=('Arial', 11), bg="#f5f5f5").pack(anchor="w", padx=12, pady=4)
tk.Label(detalle, text=f"Fecha: {fecha}", font=('Arial', 11), bg="#f5f5f5").pack(anchor="w", padx=12, pady=4)
tk.Label(detalle, text="Mensaje:", font=('Arial', 11, 'bold'), bg="#f5f5f5").pack(anchor="w", padx=12, pady=(8,0))
txt = tk.Text(detalle, width=70, height=14)
txt.pack(padx=12, pady=4)
cuerpo = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain" and not part.get('Content-Disposition'):
try:
cuerpo = part.get_payload(decode=True).decode(errors='ignore')
break
except:
pass
else:
try:
cuerpo = msg.get_payload(decode=True).decode(errors='ignore')
except:
cuerpo = msg.get_payload()
txt.insert('1.0', cuerpo)
txt.config(state='disabled')
def responder():
abrir_envio_correo(server, usuario, contrasena, reply_to=de, reply_subject=asunto)
tk.Button(detalle, text="Responder", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=responder).pack(pady=10)
tree.bind('<Double-1>', on_select)
def abrir_envio_correo(server, user, password):
envio_win = tk.Toplevel(root)
envio_win.title("Enviar Correo")
envio_win.geometry("520x400")
envio_win.resizable(False, False)
envio_win.configure(bg="#f5f5f5")
tk.Label(envio_win, text="Enviar Correo", font=('Helvetica', 15, 'bold'), bg="#f5f5f5").pack(pady=10)
frm = tk.Frame(envio_win, bg="#f5f5f5")
frm.pack(pady=8)
tk.Label(frm, text="Para:", bg="#f5f5f5").grid(row=0, column=0, sticky="e", pady=4)
entry_to = tk.Entry(frm, width=35)
entry_to.grid(row=0, column=1, pady=4)
tk.Label(frm, text="Asunto:", bg="#f5f5f5").grid(row=1, column=0, sticky="e", pady=4)
entry_subject = tk.Entry(frm, width=35)
entry_subject.grid(row=1, column=1, pady=4)
tk.Label(frm, text="Mensaje:", bg="#f5f5f5").grid(row=2, column=0, sticky="ne", pady=4)
entry_body = tk.Text(frm, width=35, height=8)
entry_body.grid(row=2, column=1, pady=4)
status = tk.Label(envio_win, text="", bg="#f5f5f5", fg="gray")
status.pack(pady=4)
def enviar():
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
to_addr = entry_to.get().strip()
subject = entry_subject.get().strip()
body = entry_body.get("1.0", "end-1c").strip()
if not all([to_addr, subject, body]):
status.config(text="Completa todos los campos", fg="red")
return
def worker():
try:
status.config(text="Enviando...", fg="blue")
msg = MIMEMultipart()
msg['From'] = user
msg['To'] = to_addr
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
with smtplib.SMTP(server, 25, timeout=10) as smtp:
try:
smtp.starttls()
except:
pass
if user and password:
try:
smtp.login(user, password)
except:
pass
smtp.send_message(msg)
status.config(text="✓ Correo enviado correctamente", fg="green")
except Exception as e:
status.config(text=f"✗ Error: {str(e)[:60]}", fg="red")
threading.Thread(target=worker, daemon=True).start()
tk.Button(envio_win, text="Enviar", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=enviar).pack(pady=10)
# Grid principal: top bar, main content, status
root.columnconfigure(0, weight=0, minsize=240)
@ -58,7 +299,10 @@ def start_client():
]
def seleccionar_categoria(nombre):
info_label.config(text=f"Categoría seleccionada: {nombre}")
if nombre == 'T4. Servicios':
abrir_ventana_correo()
else:
info_label.config(text=f"Categoría seleccionada: {nombre}")
# Creamos después info_label; el callback se ejecuta luego y tendrá acceso.
for i, (texto, color) in enumerate(categorias):
@ -530,21 +774,198 @@ def start_client():
info_label = tk.Label(info, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", bg="#f7fff0", anchor="w")
info_label.pack(fill='both', expand=True, padx=8, pady=8)
# RIGHT: Chat y lista de alumnos
chat_box = tk.LabelFrame(right, text="Chat", padx=6, pady=6)
chat_box.pack(fill="x", padx=8, pady=(8,4))
tk.Label(chat_box, text="Mensaje").pack(anchor="w")
msg = tk.Text(chat_box, height=6)
msg.pack(fill="x", pady=4)
tk.Button(chat_box, text="enviar", bg="#cfe8cf").pack(pady=(0,6))
students = tk.LabelFrame(right, text="Alumnos", padx=6, pady=6)
students.pack(fill="both", expand=True, padx=8, pady=(4,8))
for i in range(1, 4):
s = tk.Frame(students)
s.pack(fill="x", pady=6)
tk.Label(s, text=f"Alumno {i}", font=("Helvetica", 13, "bold")).pack(anchor="w")
tk.Label(s, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.", wraplength=280, justify="left").pack(anchor="w")
# Chat único en panel derecho, requiere nombre de usuario y conexión
chat_box = tk.LabelFrame(right, text="Chat - Servidor", padx=6, pady=6)
chat_box.pack(fill="both", expand=True, padx=8, pady=(4,8))
chat_history = tk.Text(chat_box, height=18, state='disabled', bg='#f5f5f5', wrap='word')
chat_history.pack(fill="both", expand=True, pady=4)
chat_history.tag_config('tu', foreground='#1565C0', font=('Arial', 9, 'bold'))
chat_history.tag_config('otro', foreground='#388E3C', font=('Arial', 9))
chat_history.tag_config('sistema', foreground='#757575', font=('Arial', 8, 'italic'))
def agregar_chat(texto, tag='sistema'):
chat_history.config(state='normal')
chat_history.insert('end', texto + '\n', tag)
chat_history.see('end')
chat_history.config(state='disabled')
# Lista de usuarios conectados
users_frame = tk.Frame(chat_box)
users_frame.pack(fill="x", pady=(0,4))
tk.Label(users_frame, text="Conectados:").pack(side="left")
users_listbox = tk.Listbox(users_frame, height=4, width=24)
users_listbox.pack(side="left", padx=(2,8))
def escribir_privado(event):
seleccion = users_listbox.curselection()
if not seleccion:
return
usuario_destino = users_listbox.get(seleccion[0])
mi_usuario = entry_user.get().strip()
if usuario_destino == mi_usuario:
return
# Ventana para mensaje privado
win = tk.Toplevel(root)
win.title(f"Mensaje privado a {usuario_destino}")
win.geometry("350x180")
tk.Label(win, text=f"Privado para: {usuario_destino}", font=('Arial', 11, 'bold')).pack(pady=8)
entry_msg = tk.Text(win, height=4)
entry_msg.pack(fill="x", padx=10, pady=6)
def enviar():
texto = entry_msg.get("1.0", "end-1c").strip()
if texto:
msg_obj = {'type': 'msg', 'from': mi_usuario, 'text': texto, 'to': usuario_destino}
try:
chat_state['socket'].send(json.dumps(msg_obj).encode('utf-8'))
agregar_chat(f"(Privado a {usuario_destino}) {mi_usuario}: {texto}", 'tu')
except Exception as e:
agregar_chat(f"Error al enviar: {e}", 'sistema')
win.destroy()
tk.Button(win, text="Enviar", bg="#cfe8cf", command=enviar).pack(pady=8)
entry_msg.focus_set()
users_listbox.bind('<Double-Button-1>', escribir_privado)
user_frame = tk.Frame(chat_box)
user_frame.pack(fill="x", pady=(0,4))
tk.Label(user_frame, text="Usuario:").pack(side="left")
entry_user = tk.Entry(user_frame, width=18)
entry_user.pack(side="left", padx=(2,8))
conn_frame = tk.Frame(chat_box)
conn_frame.pack(fill="x", pady=(0,4))
tk.Label(conn_frame, text="IP:").pack(side="left")
ip_entry = tk.Entry(conn_frame, width=12)
ip_entry.insert(0, "127.0.0.1")
ip_entry.pack(side="left", padx=(2,8))
tk.Label(conn_frame, text="Puerto:").pack(side="left")
puerto_entry = tk.Entry(conn_frame, width=6)
puerto_entry.insert(0, "3333")
puerto_entry.pack(side="left", padx=(2,8))
chat_state = {'socket': None, 'connected': False}
import json
def actualizar_usuarios(usuarios):
users_listbox.delete(0, 'end')
for u in usuarios:
users_listbox.insert('end', u)
def conectar():
if chat_state['connected']:
messagebox.showinfo("Chat", "Ya estás conectado")
return
try:
ip = ip_entry.get().strip()
puerto = int(puerto_entry.get().strip())
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, puerto))
usuario = entry_user.get().strip()
if not usuario:
messagebox.showwarning("Chat", "Debes poner un nombre de usuario")
return
# Enviar login
login_msg = json.dumps({'type': 'login', 'username': usuario}).encode('utf-8')
sock.send(login_msg)
chat_state['socket'] = sock
chat_state['connected'] = True
agregar_chat(f"Conectado a {ip}:{puerto}", 'sistema')
btn_conectar.config(text="Desconectar", bg="#ffcccc", command=desconectar)
def recibir():
while chat_state['connected']:
try:
data = sock.recv(1024)
if not data:
break
try:
msg = data.decode('utf-8')
if msg.startswith('{'):
obj = json.loads(msg)
if obj.get('type') == 'user_list':
actualizar_usuarios(obj.get('users', []))
elif obj.get('type') == 'msg':
remitente = obj.get('from', 'otro')
texto = obj.get('text', '')
es_privado = obj.get('private', False)
# Solo mostrar si el remitente no es el usuario actual
if remitente != entry_user.get().strip():
if es_privado:
agregar_chat(f"(Privado) {remitente}: {texto}", 'otro')
else:
agregar_chat(f"{remitente}: {texto}", 'otro')
else:
agregar_chat(msg, 'sistema')
else:
agregar_chat(msg, 'otro')
except Exception:
agregar_chat(data.decode('utf-8'), 'otro')
except:
break
chat_state['connected'] = False
chat_state['socket'] = None
agregar_chat("Desconectado del servidor", 'sistema')
btn_conectar.config(text="Conectar", bg="#ddeeff", command=conectar)
actualizar_usuarios([])
threading.Thread(target=recibir, daemon=True).start()
except Exception as e:
agregar_chat(f"Error: {e}", 'sistema')
def desconectar():
if chat_state['connected'] and chat_state['socket']:
try:
chat_state['socket'].close()
except:
pass
chat_state['connected'] = False
chat_state['socket'] = None
agregar_chat("Desconectado", 'sistema')
btn_conectar.config(text="Conectar", bg="#ddeeff", command=conectar)
actualizar_usuarios([])
btn_conectar = tk.Button(conn_frame, text="Conectar", bg="#ddeeff", command=conectar)
btn_conectar.pack(side="left", padx=4)
tk.Label(chat_box, text="Mensaje:").pack(anchor="w")
msg = tk.Text(chat_box, height=3)
msg.pack(fill="x", pady=4)
def enviar_mensaje(event=None):
usuario = entry_user.get().strip()
if not usuario:
messagebox.showwarning("Chat", "Debes poner un nombre de usuario")
return "break"
if not chat_state['connected']:
messagebox.showwarning("Chat", "Primero conecta al servidor")
return "break"
texto = msg.get("1.0", "end-1c").strip()
if texto:
try:
seleccion = users_listbox.curselection()
to_users = []
for idx in seleccion:
u = users_listbox.get(idx)
if u != usuario:
to_users.append(u)
msg_obj = {'type': 'msg', 'from': usuario, 'text': texto}
if len(to_users) == 1:
msg_obj['to'] = to_users[0]
agregar_chat(f"(Privado a {to_users[0]}) {usuario}: {texto}", 'tu')
elif len(to_users) > 1:
msg_obj['to'] = to_users
agregar_chat(f"(Grupo a {', '.join(to_users)}) {usuario}: {texto}", 'tu')
else:
agregar_chat(f"{usuario}: {texto}", 'tu')
chat_state['socket'].send(json.dumps(msg_obj).encode('utf-8'))
msg.delete("1.0", "end")
except Exception as e:
agregar_chat(f"Error al enviar: {e}", 'sistema')
return "break"
msg.bind('<Return>', enviar_mensaje)
tk.Button(chat_box, text="Enviar", bg="#cfe8cf", command=enviar_mensaje).pack(pady=(0,6))
# Reproductor música
music_state = {'path': None, 'thread': None, 'playing': False, 'stopping': False}
@ -598,52 +1019,6 @@ def start_client():
tk.Button(music_box, text='Play', command=reproducir_musica).pack(side='left', padx=2)
tk.Button(music_box, text='Stop', command=detener_musica).pack(side='left', padx=2)
# Chat cliente
chat_client = {'sock': None}
def conectar_chat():
if chat_client['sock']:
messagebox.showinfo('Chat','Ya conectado')
return
host = simpledialog.askstring('Host','Host chat', initialvalue='127.0.0.1')
port = simpledialog.askinteger('Puerto','Puerto', initialvalue=3333)
if not host or not port:
return
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
chat_client['sock']=s
event_queue.put(('status', f'Chat conectado {host}:{port}'))
def receptor():
try:
while True:
data = s.recv(1024)
if not data:
break
event_queue.put(('chat', data.decode(errors='ignore')))
except Exception as e:
event_queue.put(('status', f'Error chat: {e}'))
finally:
s.close(); chat_client['sock']=None
event_queue.put(('status','Chat desconectado'))
threading.Thread(target=receptor, daemon=True).start()
except Exception as e:
messagebox.showerror('Chat', f'Error conexión: {e}')
def enviar_chat():
texto = msg.get('1.0','end').strip()
if not texto:
return
s = chat_client.get('sock')
if not s:
messagebox.showwarning('Chat','No conectado')
return
try:
s.send(texto.encode())
msg.delete('1.0','end')
except Exception as e:
event_queue.put(('status', f'Error envío: {e}'))
tk.Button(chat_box, text='Conectar', bg='#ddeeff', command=conectar_chat).pack(pady=(0,4))
tk.Button(chat_box, text='Enviar mensaje', bg='#cfe8cf', command=enviar_chat).pack(pady=(0,6))
# Sección Sockets (TCP/UDP servers) añadida al panel izquierdo
s_sockets = section(left, 'Sockets Locales')
tcp_state = {'thread': None, 'stop': False, 'sock': None}
@ -805,8 +1180,112 @@ def start_client():
# Servicios placeholders
def servicio_pop3():
event_queue.put(('status','POP3 placeholder (implementación futura)'))
def servicio_smtp():
event_queue.put(('status','SMTP placeholder (implementación futura)'))
"""Ventana para enviar correo por SMTP"""
smtp_win = tk.Toplevel(root)
smtp_win.title("Enviar Correo - SMTP")
smtp_win.geometry("450x500")
smtp_win.resizable(False, False)
# Configuración del servidor
config_frame = tk.LabelFrame(smtp_win, text="Configuración SMTP", padx=8, pady=8)
config_frame.pack(fill="x", padx=10, pady=5)
tk.Label(config_frame, text="Servidor SMTP:").grid(row=0, column=0, sticky="w", pady=2)
smtp_server = tk.Entry(config_frame, width=30)
smtp_server.insert(0, "10.10.0.101")
smtp_server.grid(row=0, column=1, pady=2)
tk.Label(config_frame, text="Puerto:").grid(row=1, column=0, sticky="w", pady=2)
smtp_port = tk.Entry(config_frame, width=10)
smtp_port.insert(0, "25")
smtp_port.grid(row=1, column=1, sticky="w", pady=2)
tk.Label(config_frame, text="Tu email:").grid(row=2, column=0, sticky="w", pady=2)
smtp_user = tk.Entry(config_frame, width=30)
smtp_user.grid(row=2, column=1, pady=2)
tk.Label(config_frame, text="Contraseña:").grid(row=3, column=0, sticky="w", pady=2)
smtp_pass = tk.Entry(config_frame, width=30, show="*")
smtp_pass.grid(row=3, column=1, pady=2)
# Datos del correo
mail_frame = tk.LabelFrame(smtp_win, text="Datos del Correo", padx=8, pady=8)
mail_frame.pack(fill="x", padx=10, pady=5)
tk.Label(mail_frame, text="Para:").grid(row=0, column=0, sticky="w", pady=2)
mail_to = tk.Entry(mail_frame, width=35)
mail_to.grid(row=0, column=1, pady=2)
tk.Label(mail_frame, text="Asunto:").grid(row=1, column=0, sticky="w", pady=2)
mail_subject = tk.Entry(mail_frame, width=35)
mail_subject.grid(row=1, column=1, pady=2)
tk.Label(mail_frame, text="Mensaje:").grid(row=2, column=0, sticky="nw", pady=2)
mail_body = tk.Text(mail_frame, width=35, height=8)
mail_body.grid(row=2, column=1, pady=2)
# Estado
status_label = tk.Label(smtp_win, text="", fg="gray")
status_label.pack(pady=5)
def enviar_correo():
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
server = smtp_server.get().strip()
port = smtp_port.get().strip()
user = smtp_user.get().strip()
password = smtp_pass.get()
to_addr = mail_to.get().strip()
subject = mail_subject.get().strip()
body = mail_body.get("1.0", "end-1c").strip()
if not all([server, port, user, password, to_addr, subject, body]):
messagebox.showwarning("SMTP", "Completa todos los campos")
return
def worker():
try:
status_label.config(text="Conectando...", fg="blue")
smtp_win.update()
msg = MIMEMultipart()
msg['From'] = user
msg['To'] = to_addr
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
with smtplib.SMTP(server, int(port), timeout=10) as smtp:
# Intentar TLS si está disponible (para servidores que lo soporten)
try:
smtp.starttls()
except:
pass # Servidor local sin TLS
# Login solo si hay credenciales
if user and password:
try:
smtp.login(user, password)
except:
pass # Servidor local sin autenticación
smtp.send_message(msg)
status_label.config(text="✓ Correo enviado correctamente", fg="green")
event_queue.put(('status', f'Correo enviado a {to_addr}'))
except Exception as e:
status_label.config(text=f"✗ Error: {str(e)[:50]}", fg="red")
event_queue.put(('status', f'Error SMTP: {e}'))
threading.Thread(target=worker, daemon=True).start()
btn_frame = tk.Frame(smtp_win)
btn_frame.pack(pady=10)
tk.Button(btn_frame, text="Enviar Correo", bg="#90EE90", font=('Arial', 10, 'bold'),
command=enviar_correo).pack(side="left", padx=5)
tk.Button(btn_frame, text="Cerrar", command=smtp_win.destroy).pack(side="left", padx=5)
def servicio_ftp():
event_queue.put(('status','FTP placeholder (implementación futura)'))

191
main.py Normal file → Executable file
View File

@ -1,54 +1,159 @@
import socket
import threading
import json
# Configuraci n del servidor
HOST = '0.0.0.0' # Escucha en todas las interfaces de red
PORT = 3333 # Puerto de escucha
clients = [] # Lista para almacenar los clientes conectados
class ChatServer:
def __init__(self, host='0.0.0.0', port=3333):
self.host = host
self.port = port
self.clients = [] # [{'socket':..., 'address':..., 'username':...}]
self.server = None
# Funci n para retransmitir mensajes a todos los clientes
def broadcast(message, client_socket):
for client in clients:
if client != client_socket: # Evitar enviar el mensaje al remitente
def start(self):
import random
import time
max_attempts = 10
attempt = 0
port_ok = False
while attempt < max_attempts:
try:
client.send(message)
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((self.host, self.port))
port_ok = True
break
except OSError as e:
print(f"[ERROR] Puerto {self.port} ocupado. Intentando otro...")
self.port = random.randint(20000, 60000)
attempt += 1
time.sleep(0.5)
if not port_ok:
while True:
try:
user_port = input("Introduce un puerto libre para el servidor: ")
self.port = int(user_port)
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((self.host, self.port))
break
except Exception as e:
print(f"[ERROR] No se pudo usar el puerto {self.port}: {e}")
self.server.listen(5)
print(f"[INICIO] Servidor escuchando en {self.host}:{self.port}")
while True:
client_socket, client_address = self.server.accept()
t = threading.Thread(target=self.handle_client, args=(client_socket, client_address))
t.start()
def broadcast(self, message, exclude_socket=None):
for client in self.clients:
if client['socket'] != exclude_socket:
try:
client['socket'].send(message)
except:
try:
client['socket'].close()
except:
pass
self.clients.remove(client)
def send_user_list(self):
user_list = [c['username'] for c in self.clients if c.get('username')]
data = {'type': 'user_list', 'users': user_list}
msg = json.dumps(data).encode('utf-8')
for client in self.clients:
try:
client['socket'].send(msg)
except:
# Si falla el env o, eliminar el cliente
clients.remove(client)
pass
# Función para manejar la comunicación con un cliente
def handle_client(client_socket, client_address):
print(f"[NUEVO CLIENTE] {client_address} conectado.")
while True:
def handle_client(self, client_socket, client_address):
print(f"[NUEVO CLIENTE] {client_address} conectado.")
username = None
try:
# Recibir mensaje del cliente
message = client_socket.recv(1024)
if not message:
break # Si no hay mensaje, el cliente cerr la conexi n
print(f"[{client_address}] {message.decode('utf-8')}")
# Retransmitir el mensaje a los dem s clientes
broadcast(message, client_socket)
except:
print(f"[DESCONECTADO] {client_address} se ha desconectado.")
clients.remove(client_socket)
data = client_socket.recv(1024)
if not data:
client_socket.close()
return
try:
msg = data.decode('utf-8')
if msg.startswith('{'):
obj = json.loads(msg)
if obj.get('type') == 'login' and obj.get('username'):
username = obj['username']
except Exception:
pass
if not username:
client_socket.send(b'Usuario no proporcionado. Desconectando.')
client_socket.close()
return
# Comprobar si el nombre solo contiene letras del abecedario inglés
# Limpiar espacios y asegurar tipo string
if not isinstance(username, str):
client_socket.send(b'Nombre de usuario invalido. Solo letras A-Z permitidas. Desconectando.')
client_socket.close()
return
username = username.strip()
# Permitir solo letras y espacios
if not username or not username.isascii() or not all(c.isalpha() or c == ' ' for c in username):
client_socket.send(b'Nombre de usuario invalido. Solo letras A-Z y espacios permitidos. Desconectando.')
client_socket.close()
return
# Comprobar si el nombre ya está en uso
if any(c['username'] == username for c in self.clients):
client_socket.send(b'Nombre de usuario en uso. Desconectando.')
client_socket.close()
return
client_info = {'socket': client_socket, 'address': client_address, 'username': username}
self.clients.append(client_info)
print(f"[LOGIN] {username} desde {client_address}")
self.send_user_list()
while True:
message = client_socket.recv(1024)
if not message:
break
try:
msg = message.decode('utf-8')
if msg.startswith('{'):
obj = json.loads(msg)
if obj.get('type') == 'msg':
texto = obj.get('text', '')
remitente = obj.get('from', username)
para = obj.get('to')
if para:
# Mensaje privado
for c in self.clients:
if c['username'] == para or (isinstance(para, list) and c['username'] in para):
try:
privado = obj.copy()
privado['private'] = True
c['socket'].send(json.dumps(privado).encode('utf-8'))
except:
pass
# También enviar copia al remitente
for c in self.clients:
if c['username'] == remitente:
try:
privado = obj.copy()
privado['private'] = True
c['socket'].send(json.dumps(privado).encode('utf-8'))
except:
pass
else:
obj['private'] = False
# Enviar a todos, incluido el remitente
self.broadcast(json.dumps(obj).encode('utf-8'), exclude_socket=None)
else:
# Mensaje no JSON, reenviar a todos, incluido el remitente
self.broadcast(message, exclude_socket=None)
except Exception as e:
print(f"[ERROR] {e}")
except Exception as e:
print(f"[DESCONECTADO] {client_address} se ha desconectado. Error: {e}")
finally:
for c in self.clients[:]:
if c['socket'] == client_socket:
self.clients.remove(c)
client_socket.close()
break
self.send_user_list()
# Funci n principal para iniciar el servidor
def start_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4, TCP
server.bind((HOST, PORT))
server.listen(5) # M ximo 5 conexiones en cola
print(f"[INICIO] Servidor escuchando en {HOST}:{PORT}")
while True:
client_socket, client_address = server.accept() # Aceptar nueva conexión
clients.append(client_socket)
print(f"[CONECTADO] Nueva conexi n desde {client_address}")
# Iniciar un hilo para manejar el cliente
client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
client_thread.start()
# Iniciar el servidor
if __name__ == "__main__":
start_server()
ChatServer().start()

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
# Dependencias del proyecto PSP
# Web scraping
requests
beautifulsoup4
# Recursos del sistema
psutil
# Audio
pygame
# Criptografía
cryptography