From f9c3bc17026fb659e442254f83e4a6aa41739b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Fri, 27 Feb 2026 18:36:12 +0100 Subject: [PATCH] correos y chat mejorado --- ReadMe.md | 0 __pycache__/client.cpython-313.pyc | Bin 68212 -> 0 bytes client.py | 603 ++++++++++++++++++++++++++--- main.py | 191 +++++++-- requirements.txt | 14 + 5 files changed, 703 insertions(+), 105 deletions(-) mode change 100644 => 100755 ReadMe.md delete mode 100644 __pycache__/client.cpython-313.pyc mode change 100644 => 100755 client.py mode change 100644 => 100755 main.py create mode 100644 requirements.txt diff --git a/ReadMe.md b/ReadMe.md old mode 100644 new mode 100755 diff --git a/__pycache__/client.cpython-313.pyc b/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 7ec74d486a879d887606b52b6d5743943086e919..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68212 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 T8&*X8V2ieXoBD$SCGP(ZKgJf0 diff --git a/client.py b/client.py old mode 100644 new mode 100755 index 32db9fc..8feaa81 --- a/client.py +++ b/client.py @@ -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('', 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('', 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('', 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)')) diff --git a/main.py b/main.py old mode 100644 new mode 100755 index b407215..fc21128 --- a/main.py +++ b/main.py @@ -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() \ No newline at end of file + ChatServer().start() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0d153f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +# Dependencias del proyecto PSP + +# Web scraping +requests +beautifulsoup4 + +# Recursos del sistema +psutil + +# Audio +pygame + +# Criptografía +cryptography