Added Mail interface and POP client, pop connections still needs to be closes when program is finished

This commit is contained in:
Dennis Eckerskorn 2025-01-14 21:54:03 +01:00
parent 128ab84429
commit 36e1a225e6
11 changed files with 341 additions and 100 deletions

View File

@ -2,11 +2,7 @@
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module> </module>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.12" /> <option name="sdkName" value="Python 3.13" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
</project> </project>

Binary file not shown.

View File

@ -8,10 +8,11 @@ from email.mime.multipart import MIMEMultipart
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
class EmailClientPOP: class EmailClientPOP:
def __init__(self, pop_server, smtp_server, email, password, pop_port = 110, smtp_port=25): def __init__(self, pop_server, smtp_server, email, password, pop_port=110, smtp_port=25):
self.pop_server = pop_server self.pop_server = pop_server
self.smtp = smtp_server self.smtp_server = smtp_server
self.email = email self.email = email
self.password = password self.password = password
self.pop_port = pop_port self.pop_port = pop_port
@ -19,7 +20,7 @@ class EmailClientPOP:
self.pop_conn = None self.pop_conn = None
self.smtp_conn = None self.smtp_conn = None
#Ruta del archivo SQLite: # Ruta del archivo SQLite:
self.db_file = os.path.join("resources/db_email", "emails.db") self.db_file = os.path.join("resources/db_email", "emails.db")
self.init_database() self.init_database()
@ -40,8 +41,169 @@ class EmailClientPOP:
received_at DATETIME received_at DATETIME
) )
""") """)
cursor.execute("""
CREATE TABLE IF NOT EXISTS sent_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipient TEXT NOT NULL,
subject TEXT,
body TEXT,
sent_at DATETIME
)
""")
conn.commit() conn.commit()
conn.close() conn.close()
print(f"Base de datos inicializada: {self.db_file}") print(f"Base de datos inicializada: {self.db_file}")
except sqlite3.Error as e: except sqlite3.Error as e:
print(f"Error al inicializar la base de datos: {e}") print(f"Error al inicializar la base de datos: {e}")
def connect_pop(self):
"""Conexión al servidor POP"""
try:
self.pop_conn = poplib.POP3(self.pop_server, self.pop_port)
self.pop_conn.user(self.email)
self.pop_conn.pass_(self.password)
print("Conexión POP exitosa")
except Exception as e:
print(f"Error al conectar al servidor POP: {e}")
self.pop_conn = None
def connect_smtp(self):
"""Conexión al servidor SMTP"""
try:
self.smtp_conn = smtplib.SMTP(self.smtp_server, self.smtp_port)
self.smtp_conn.login(self.email, self.password)
print("Conexión SMTP exitosa")
except Exception as e:
print(f"Error al conectar al servidor SMTP: {e}")
self.smtp_conn = None
def is_connected(self):
"""Verifica si hay una conexión válida tanto de POP como de SMTP"""
return self.pop_conn is not None and self.smtp_conn is not None
def reconnect(self):
"""Intenta reconectar a los servidores POP y SMTP"""
print("Intentando reconectar al servidor de correo...")
self.connect_pop()
self.connect_smtp()
def fetch_unread_count(self):
"""Obtener el número de correos (POP3 no distingue entre leídos y no leídos)"""
try:
if not self.pop_conn:
self.connect_pop()
num_messages = len(self.pop_conn.list()[1])
return num_messages # Total de mensajes
except Exception as e:
print(f"Error al obtener el conteo de correos: {e}")
return 0
def fetch_emails(self, save_to_db=True):
"""Obtiene correos desde el servidor POP"""
try:
if not self.pop_conn:
self.connect_pop()
emails = []
num_messages = len(self.pop_conn.list()[1])
for i in range(1, num_messages + 1):
raw_messages = b"\n".join(self.pop_conn.retr(i)[1])
msg = email.message_from_bytes(raw_messages)
subject, encoding = decode_header(msg["Subject"])[0]
if isinstance(subject, bytes):
subject = subject.decode(encoding or "utf-8")
sender = msg.get("From")
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
body = part.get_payload(decode=True).decode("utf-8", errors="ignore")
else:
body = msg.get_payload(decode=True).decode("utf-8", errors="ignore")
email_data = {
"sender": sender,
"subject": subject,
"body": body,
"received_at": datetime.now().isoformat()
}
emails.append(email_data)
if save_to_db:
self.save_email_to_db(email_data)
return emails
except Exception as e:
print(f"Error al obtener los correos: {e}")
return []
def fetch_folders(self):
"""POP3 no tiene carpetas, devuelve un mensaje indicando esto"""
print("El protocolo POP3 no soporta carpetas. Devuelve solo la bandeja de entrada.")
return ["INBOX"]
def save_email_to_db(self, email_data):
"""Guarda un correo recibido en la base de datos."""
try:
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO emails (sender, subject, body, received_at)
VALUES (?, ?, ?, ?)
""", (email_data["sender"], email_data["subject"], email_data["body"], email_data["received_at"]))
conn.commit()
conn.close()
print("Correo recibido guardado en la base de datos.")
except sqlite3.Error as e:
print(f"Error al guardar correo recibido: {e}")
def send_mail(self, recipient, subject, body, save_to_db=True):
"""Envía un correo usando SMTP"""
try:
if not self.smtp_conn:
self.connect_smtp()
msg = MIMEMultipart()
msg["From"] = self.email
msg["To"] = recipient
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
self.smtp_conn.sendmail(self.email, recipient, msg.as_string())
print(f"Correo ha sido enviado a {recipient}")
if save_to_db:
self.save_sent_mail_to_db({
"recipient": recipient,
"subject": subject,
"body": body,
"sent_at": datetime.now().isoformat()
})
except Exception as e:
print(f"Error al enviar correo: {e}")
def save_sent_mail_to_db(self, email_data):
"""Guarda un correo enviado en la base de datos."""
try:
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO sent_emails (recipient, subject, body, sent_at)
VALUES (?, ?, ?, ?)
""", (email_data["recipient"], email_data["subject"], email_data["body"], email_data["sent_at"]))
conn.commit()
conn.close()
print("Correo enviado guardado en la base de datos.")
except sqlite3.Error as e:
print(f"Error al guardar correo enviado: {e}")
def list_emails(self, limit=10):
"""Lista correos recientes (máximo `limit`)"""
emails = self.fetch_emails()
return emails[:limit]
def close_connections(self):
"""Cierra las conexiones POP y SMTP"""
if self.pop_conn:
self.pop_conn.quit()
if self.smtp_conn:
self.smtp_conn.quit()

View File

@ -6,7 +6,7 @@ from src.services.processes_manager import ProcessManager
from src.services.system_monitor import SystemMonitor from src.services.system_monitor import SystemMonitor
from src.services.tetris_game import TetrisGame from src.services.tetris_game import TetrisGame
from src.services.threads_manager import ThreadsManager from src.services.threads_manager import ThreadsManager
from src.services.email_client_imap import EmailClientImap from src.services.email_client_pop import EmailClientPOP
class CenteredWindow(ctk.CTk): class CenteredWindow(ctk.CTk):
@ -16,9 +16,9 @@ class CenteredWindow(ctk.CTk):
self.title(title) self.title(title)
self.after_tasks = [] self.after_tasks = []
#Configurar Email Client # Configurar Email Client IMAP
self.email_client = EmailClientImap( self.email_client = EmailClientPOP(
imap_server="192.168.120.103", pop_server="192.168.120.103",
smtp_server="192.168.120.103", smtp_server="192.168.120.103",
email="dennis@psp.ieslamar.org", email="dennis@psp.ieslamar.org",
password="1234" password="1234"
@ -28,21 +28,21 @@ class CenteredWindow(ctk.CTk):
self.process_manager = ProcessManager() self.process_manager = ProcessManager()
self.system_monitor = None self.system_monitor = None
# Obtener la resolucion de la pantalla: # Obtener la resolución de la pantalla:
screen_width = self.winfo_screenwidth() screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight() screen_height = self.winfo_screenheight()
# Calcula el tamaño de la ventana según procentaje de la pantalla: # Calcula el tamaño de la ventana según porcentaje de la pantalla:
window_width = int(screen_width * width_percentage) window_width = int(screen_width * width_percentage)
window_height = int(screen_height * height_percentage) window_height = int(screen_height * height_percentage)
# Calcular la posicion para centrar la ventana: # Calcular la posición para centrar la ventana:
position_x = (screen_width - window_width) // 2 position_x = (screen_width - window_width) // 2
position_y = (screen_height - window_height) // 2 position_y = (screen_height - window_height) // 2
self.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}") self.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}")
#Configura la ventana # Configura la ventana
self.configure_window() self.configure_window()
self.protocol("WM_DELETE_WINDOW", self.on_close) self.protocol("WM_DELETE_WINDOW", self.on_close)
@ -62,7 +62,6 @@ class CenteredWindow(ctk.CTk):
self.thread_manager.start_threads() self.thread_manager.start_threads()
def on_close(self): def on_close(self):
"""Maneja el cierre de la ventana""" """Maneja el cierre de la ventana"""
self.thread_manager.stop_threads() self.thread_manager.stop_threads()
@ -91,10 +90,16 @@ class CenteredWindow(ctk.CTk):
# Secciones y botones # Secciones y botones
sections = { sections = {
"Aplicaciones": [ "Aplicaciones": [
("Abrir Chrome", lambda: self.process_manager.open_resource("browser", "https://google.com", "Cannot open browser")), ("Abrir Chrome",
("Visual Studio Code", lambda: self.process_manager.open_resource("program", r"C:\Program Files\Microsoft VS Code\Code.exe", "Can't find VSCode")), lambda: self.process_manager.open_resource("browser", "https://google.com", "Cannot open browser")),
("Explorador de Windows", lambda: self.process_manager.open_resource("program", "explorer.exe", "Can't open Windows Explorer")), ("Visual Studio Code",
("Notepad++", lambda: self.process_manager.open_resource("program", r"C:\Program Files\Notepad++\notepad++.exe", "Can't open Notepad++")) lambda: self.process_manager.open_resource("program", r"C:\Program Files\Microsoft VS Code\Code.exe",
"Can't find VSCode")),
("Explorador de Windows",
lambda: self.process_manager.open_resource("program", "explorer.exe", "Can't open Windows Explorer")),
("Notepad++",
lambda: self.process_manager.open_resource("program", r"C:\Program Files\Notepad++\notepad++.exe",
"Can't open Notepad++"))
] ]
} }
@ -128,14 +133,14 @@ class CenteredWindow(ctk.CTk):
self.left_panel = left_panel self.left_panel = left_panel
self.left_panel.url_entry = url_entry self.left_panel.url_entry = url_entry
self.left_panel.url_entry_chrome = url_entry_chrome self.left_panel.url_entry_chrome = url_entry_chrome
start_button = ctk.CTkButton(left_panel, text="Iniciar Scrapping", command=self.thread_manager.scrapper.start_scraping) start_button = ctk.CTkButton(left_panel, text="Iniciar Scrapping",
command=self.thread_manager.scrapper.start_scraping)
start_button.pack(pady=5, padx=10) start_button.pack(pady=5, padx=10)
stop_button = ctk.CTkButton(left_panel, text="Detener Scrapping", command=self.thread_manager.scrapper.stop_scraping) stop_button = ctk.CTkButton(left_panel, text="Detener Scrapping",
command=self.thread_manager.scrapper.stop_scraping)
stop_button.pack(pady=5, padx=10) stop_button.pack(pady=5, padx=10)
def create_center_panel(self): def create_center_panel(self):
# Panel central con pestañas # Panel central con pestañas
center_panel = ctk.CTkFrame(self) center_panel = ctk.CTkFrame(self)
@ -151,6 +156,9 @@ class CenteredWindow(ctk.CTk):
if tab_name == "Radio": if tab_name == "Radio":
self.create_radio_tab(tab) self.create_radio_tab(tab)
if tab_name == "Correos":
self.create_email_tab(tab)
if tab_name == "Scrapping": if tab_name == "Scrapping":
text_widget = ctk.CTkTextbox(tab, width=500, height=400) text_widget = ctk.CTkTextbox(tab, width=500, height=400)
text_widget.pack(fill=ctk.BOTH, expand=True, padx=10, pady=10) text_widget.pack(fill=ctk.BOTH, expand=True, padx=10, pady=10)
@ -192,33 +200,25 @@ class CenteredWindow(ctk.CTk):
self.tetris_game = TetrisGame(game_frame) self.tetris_game = TetrisGame(game_frame)
self.tetris_game.pack() self.tetris_game.pack()
# else: # else:
# Agregar contenido genérico a otras pestañas # Agregar contenido genérico a otras pestañas
#label = ctk.CTkLabel(tab, text=f"Contenido de {tab_name}", font=("Arial", 12)) # label = ctk.CTkLabel(tab, text=f"Contenido de {tab_name}", font=("Arial", 12))
#label.pack(pady=10) # label.pack(pady=10)
def start_tetris_game(self): def start_tetris_game(self):
"""Método para iniciar el juego.""" """Método para iniciar el juego."""
if not self.tetris_game.running: if not self.tetris_game.running:
self.tetris_game.running = True self.tetris_game.running = True
#self.tetris_game.update_game() # self.tetris_game.update_game()
def pause_tetris_game(self): def pause_tetris_game(self):
"""Método para pausar el juego.""" """Método para pausar el juego."""
self.tetris_game.running = False self.tetris_game.running = False
def restart_tetris_game(self): def restart_tetris_game(self):
"""Método para reiniciar el juego.""" """Método para reiniciar el juego."""
self.tetris_game.reset_game() self.tetris_game.reset_game()
def create_right_panel(self): def create_right_panel(self):
# Panel derecho # Panel derecho
right_panel = ctk.CTkFrame(self, width=250) right_panel = ctk.CTkFrame(self, width=250)
@ -236,7 +236,8 @@ class CenteredWindow(ctk.CTk):
# Lista de alumnos # Lista de alumnos
for i in range(1, 4): for i in range(1, 4):
student_label = ctk.CTkLabel(right_panel, text=f"Alumno {i}", font=("Arial", 12, "bold"), text_color="black") student_label = ctk.CTkLabel(right_panel, text=f"Alumno {i}", font=("Arial", 12, "bold"),
text_color="black")
student_label.pack(anchor=ctk.W, pady=5, padx=10) student_label.pack(anchor=ctk.W, pady=5, padx=10)
student_info = ctk.CTkLabel( student_info = ctk.CTkLabel(
@ -247,8 +248,6 @@ class CenteredWindow(ctk.CTk):
) )
student_info.pack(anchor=ctk.W, padx=10) student_info.pack(anchor=ctk.W, padx=10)
def create_bottom_bar(self): def create_bottom_bar(self):
# Crear la barra inferior # Crear la barra inferior
self.bottom_bar = ctk.CTkFrame(self, fg_color="lightblue", height=40) self.bottom_bar = ctk.CTkFrame(self, fg_color="lightblue", height=40)
@ -258,7 +257,8 @@ class CenteredWindow(ctk.CTk):
self.info_labels = { self.info_labels = {
"hora": ctk.CTkLabel(self.bottom_bar, text="Hora: --:--:--", font=("Arial", 12), text_color="black"), "hora": ctk.CTkLabel(self.bottom_bar, text="Hora: --:--:--", font=("Arial", 12), text_color="black"),
"fecha": ctk.CTkLabel(self.bottom_bar, text="Fecha: --/--/----", font=("Arial", 12), text_color="black"), "fecha": ctk.CTkLabel(self.bottom_bar, text="Fecha: --/--/----", font=("Arial", 12), text_color="black"),
"temperatura": ctk.CTkLabel(self.bottom_bar, text="Temperatura local: --°C", font=("Arial", 12), text_color="black"), "temperatura": ctk.CTkLabel(self.bottom_bar, text="Temperatura local: --°C", font=("Arial", 12),
text_color="black"),
"emails": ctk.CTkLabel(self.bottom_bar, text="Correos sin leer: 0", font=("Arial", 12), text_color="black"), "emails": ctk.CTkLabel(self.bottom_bar, text="Correos sin leer: 0", font=("Arial", 12), text_color="black"),
} }
@ -266,8 +266,6 @@ class CenteredWindow(ctk.CTk):
for label in self.info_labels.values(): for label in self.info_labels.values():
label.pack(side=ctk.LEFT, padx=10, pady=5) label.pack(side=ctk.LEFT, padx=10, pady=5)
def dummy_action(self): def dummy_action(self):
print("Acción no implementada") print("Acción no implementada")
@ -321,3 +319,88 @@ class CenteredWindow(ctk.CTk):
self.radio_player.stop() self.radio_player.stop()
self.radio_controls["play_button"].configure(state=tk.NORMAL) self.radio_controls["play_button"].configure(state=tk.NORMAL)
self.radio_controls["stop_button"].configure(state=tk.DISABLED) self.radio_controls["stop_button"].configure(state=tk.DISABLED)
def create_email_tab(self, tab):
"""Crea una interfaz moderna para gestionar los correos con customtkinter."""
# Configurar el grid para permitir que los elementos se expandan
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(0, weight=1)
# Crear un marco principal para la pestaña
main_frame = ctk.CTkFrame(tab)
main_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
# Lista de correos en un marco
listbox_frame = ctk.CTkFrame(main_frame)
listbox_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
# Crear una lista de correos con un scrollbar
self.email_listbox = ctk.CTkTextbox(listbox_frame, width=800, height=800)
self.email_listbox.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
self.email_listbox.configure(state="disabled") # Inicialmente deshabilitada
scrollbar = ctk.CTkScrollbar(listbox_frame, command=self.email_listbox.yview)
scrollbar.grid(row=0, column=1, sticky="ns")
self.email_listbox.configure(yscrollcommand=scrollbar.set)
# Frame para botones de acción
button_frame = ctk.CTkFrame(main_frame)
button_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=10)
# Crear botones para acciones: Marcar como leído, Eliminar, Actualizar
mark_read_button = ctk.CTkButton(
button_frame, text="Marcar como leído", command=self.mark_email_as_read, fg_color="green"
)
mark_read_button.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
delete_button = ctk.CTkButton(
button_frame, text="Eliminar correo", command=self.delete_email, fg_color="red"
)
delete_button.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
refresh_button = ctk.CTkButton(
button_frame, text="Actualizar", command=self.refresh_email_list, fg_color="blue"
)
refresh_button.grid(row=0, column=2, padx=5, pady=5, sticky="ew")
# Expandir las filas y columnas del marco principal
main_frame.grid_rowconfigure(0, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
def refresh_email_list(self):
"""Actualiza la lista de correos en la interfaz."""
try:
if not self.email_client.is_connected():
self.email_client.reconnect()
if self.email_client.is_connected():
emails = self.email_client.fetch_emails()
self.email_listbox.delete(0, tk.END)
for email in emails:
self.email_listbox.insert(tk.END, f"{email['subject']} - {email['from']}")
else:
print("No hay conexión al servidor de correo.")
except Exception as e:
print(f"Error al actualizar la lista de correos: {e}")
def mark_email_as_read(self):
"""Marca el correo seleccionado como leído."""
selected_index = self.email_listbox.curselection()
if not selected_index:
print("No se ha seleccionado ningún correo.")
return
selected_email = self.email_listbox.get(selected_index)
# Aquí puedes agregar la lógica para marcar el correo como leído
print(f"Correo marcado como leído: {selected_email}")
def delete_email(self):
"""Elimina el correo seleccionado."""
selected_index = self.email_listbox.curselection()
if not selected_index:
print("No se ha seleccionado ningún correo.")
return
selected_email = self.email_listbox.get(selected_index)
# Aquí puedes agregar la lógica para eliminar el correo
print(f"Correo eliminado: {selected_email}")