- Add specific line references to servidor.py code blocks - Add specific line references to app.py code blocks - Improve code traceability for email client documentation - Improve code traceability for game server documentation - Make documentation more navigable with precise locations |
||
|---|---|---|
| .gitignore | ||
| ARQUITECTURA_CORREO.md | ||
| CORREO_README.md | ||
| README.md | ||
| TESTING_GUIDE.md | ||
| app.py | ||
| cliente_juego.py | ||
| requirements.txt | ||
| servidor.py | ||
| test_mail_server.py | ||
| test_startup.py | ||
README.md
ðĢ Minesweeper Multiplayer + Dashboard
Un proyecto completo de programaciÃģn de servicios y procesos
Juego de buscaminas competitivo en red + Panel de control integral
CaracterÃsticas âĒ Arquitectura âĒ InstalaciÃģn âĒ Uso âĒ MecÃĄnicas âĒ TecnologÃas
ðļ Vista Previa
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ðŪ MINESWEEPER MULTIPLAYER DASHBOARD â
ââââââââââââââââŽâââââââââââââââââââââââââââââââââââââââŽââââââââââââââââââââââââĪ
â â â âââââââââââââââââââ â
â ACCIONES â ÃREA DE RESULTADOS â â ðĢ MINESWEEPER â â
â ââââââââââ â â â MULTIPLAYER â â
â â ââââââââââââââââââââââââââââââ â âââââââââââââââââââĪ â
â > Wallapop â â ð Monitor Sistema â â â Ronda: 3 â â
â > Scraping â â ð CPU: 45% â â â ðĢ Bombas: 9 â â
â > API Tiempo â â ðū RAM: 2.1GB â â â âĪïļ Vidas: 2 â â
â â ââââââââââââââââââââââââââââââ â âââââââââââââââââââĪ â
â APPS â â â âââââŽââââŽââââ â â
â ââââââââââ â Tabs: [Resultados][Navegador] â â â âĒ â âĒ â â â â â
â > VS Code â [Correos][Bloc][Tareas] â â âââââžââââžââââĪ â â
â > Camellos â [Alarmas][Enlaces] â â â ðĨâ âĒ â âĒ â â â
â â â â âââââīââââīââââ â â
â BATCH â ââââââââââââââââââââââââââââââ â âââââââââââââââââââĪ â
â ââââââââââ â â ð Panel de Notas â â â [Iniciar Juego] â â
â > Backups â â â â â [Zona Despejada]â â
â â ââââââââââââââââââââââââââââââ â âââââââââââââââââââ â
ââââââââââââââââīâââââââââââââââââââââââââââââââââââââââīââââââââââââââââââââââââ
âĻ CaracterÃsticas
ðŪ Juego Minesweeper Multijugador
| CaracterÃstica | DescripciÃģn |
|---|---|
| ðĨ Competitivo | 2+ jugadores compiten en tiempo real |
| ðĢ ColocaciÃģn estratÃĐgica | Cada jugador coloca bombas para el rival |
| ð Por turnos | Sistema de turnos para colocar y buscar |
| ð Dificultad progresiva | 5 rondas con grids y bombas crecientes |
| âĪïļ Sistema de vidas | 3 vidas por partida, ÂĄno las pierdas! |
ð Dashboard Integral
- ðĄ Monitor del sistema en tiempo real (CPU, RAM, hilos)
- ðĪïļ API del tiempo para JÃĄvea (OpenWeather)
- ð AnÃĄlisis de Wallapop - Scraping de anuncios
- â° Sistema de alarmas programables
- ð Bloc de notas integrado
- ð Gestor de enlaces rÃĄpidos
- ðē Minijuego de camellos con animaciones
ð§ Cliente de Correo ElectrÃģnico Completo
| CaracterÃstica | DescripciÃģn |
|---|---|
| ðŽ IMAP | Lectura de correos desde servidor Webmin (Puerto 143) |
| ðĪ SMTP | EnvÃo de correos con autenticaciÃģn (Puerto 25) |
| ðū Auto-guardado | Credenciales guardadas con Base64 |
| ðĨ MÚltiples destinatarios | EnvÃo a varios correos simultÃĄneamente |
| ð Adjuntos | Soporte para imÃĄgenes, PDFs, docs, Excel, ZIP |
| ðžïļ ImÃĄgenes inline | VisualizaciÃģn de imÃĄgenes dentro del correo |
| ðïļ Carpetas | Bandeja de entrada y enviados sincronizados |
| ðĩ Indicadores | Correos leÃdos/no leÃdos con marcadores visuales |
ðïļ Arquitectura
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ARQUITECTURA DEL SISTEMA â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ââââââââââââââââââââââââââââ
â SERVIDOR TCP â
â servidor.py â
â ââââââââââââââââââââ â
â â GameServer â â
â â - Estado juego â â
â â - Broadcast â â
â â - Turnos â â
â ââââââââââââââââââââ â
â Puerto 3333 â
ââââââââââââââŽââââââââââââââ
â
ââââââââââââââīââââââââââââââââ
â Protocolo JSON â
â sobre TCP/IP â
ââââââââââââââŽââââââââââââââââ
âââââââââââââââââââââââžââââââââââââââââââââââ
â â â
âž âž âž
ââââââââââââââââââââ ââââââââââââââââââââ ââââââââââââââââââââ
â CLIENTE 1 â â CLIENTE 2 â â CLIENTE N â
â â â â â â
â ââââââââââââââ â â ââââââââââââââ â â ââââââââââââââ â
â â app.py â â â â app.py â â â â cliente_ â â
â â Dashboard â â â â Dashboard â â â â juego.py â â
â â + Juego â â â â + Juego â â â â Standalone â â
â ââââââââââââââ â â ââââââââââââââ â â ââââââââââââââ â
ââââââââââââââââââââ ââââââââââââââââââââ ââââââââââââââââââââ
ð Estructura del Proyecto
Proyecto1AVApsp/
â
âââ ð servidor.py # Servidor TCP del juego (371 lÃneas)
â âââ GameServer # Gestiona estado, turnos y broadcasts
â
âââ ð app.py # Dashboard principal (2566 lÃneas)
â âââ DashboardApp # AplicaciÃģn Tkinter completa
â âââ GameClient # Cliente TCP integrado
â
âââ ð cliente_juego.py # Cliente standalone (220 lÃneas)
â âââ GameClient # VersiÃģn ligera para jugar
â
âââ ð requirements.txt # Dependencias Python
âââ ð README.md # Este archivo
ð InstalaciÃģn
Requisitos Previos
- Python 3.8+
- pip (gestor de paquetes)
Paso 1: Clonar el Repositorio
git clone https://github.com/MarcosFerrandiz/Proyecto1AVApsp.git
cd Proyecto1AVApsp
Paso 2: Crear Entorno Virtual (Recomendado)
python -m venv .venv
# Linux/macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
Paso 3: Instalar Dependencias
pip install -r requirements.txt
ðĶ Ver dependencias detalladas
| Paquete | VersiÃģn | Uso |
|---|---|---|
psutil |
âĨ5.9.0 | Monitor de recursos del sistema |
matplotlib |
âĨ3.5.0 | GrÃĄficos en tiempo real |
pillow |
âĨ9.0.0 | Procesamiento de imÃĄgenes |
pygame |
âĨ2.1.0 | ReproducciÃģn de audio (opcional) |
requests |
âĨ2.32.0 | Peticiones HTTP (API, Scraping) |
beautifulsoup4 |
âĨ4.12.0 | Parsing HTML (Scraping) |
ðŊ Uso
ðĨïļ Iniciar el Servidor
python servidor.py
El servidor escucharÃĄ en
0.0.0.0:3333
ðŪ OpciÃģn A: Dashboard Completo
python app.py
Incluye el juego integrado + todas las funcionalidades del panel.
ðŪ OpciÃģn B: Cliente Standalone
python cliente_juego.py
Cliente ligero solo para jugar al Minesweeper.
ð Conectar al Juego
- Introduce el Host del servidor (por defecto:
127.0.0.1) - Verifica el Puerto (por defecto:
3333) - Pulsa "Conectar"
- ÂĄEspera a otro jugador y pulsa "Iniciar Juego"!
ðĢ MecÃĄnicas del Juego
ð Flujo de una Partida
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO DEL JUEGO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â âââââââââââ âââââââââââââââ âââââââââââââââ â
â â LOBBY ââââââķâ COLOCACIÃN ââââââķâ BÃSQUEDA â â
â â â â DE BOMBAS â â (PLAYING) â â
â âââââââââââ âââââââââââââââ ââââââââŽâââââââ â
â â â â
â â âââââââââââââââââââââââââââââ â
â â â â
â â âž â
â â âââââââââââ ââââââââââââ ââââââââââââââ â
â â â ÂŋBOMBA? âââââķâ EXPLOSIÃNâââââķâ ÂŋVIDAS=0? â â
â â ââââââŽâââââ ââââââââââââ âââââââŽâââââââ â
â â â NO â â
â â âž SÃ âž â
â â âââââââââââ ââââââââââââ â
â â â SAFE â â GAME OVERâ â
â â â(casilla â ââââââââââââ â
â â â segura) â â
â â ââââââŽâââââ â
â â â â
â â âž â
â â âŋâââââââââââââââââââââââââââââââââââââ â
â â â SIGUIENTE TURNO â â
â â ââââââââââââââââââââââââââââââââââââââ â
â â â
â âââââââââââââââââââââķ (Nueva Partida) â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ð ProgresiÃģn de Dificultad
| Ronda | TamaÃąo Grid | Bombas/Jugador | Total Bombas* |
|---|---|---|---|
| 1ïļâĢ | 3Ã3 | 3 | 6 |
| 2ïļâĢ | 5Ã5 | 5 | 10 |
| 3ïļâĢ | 8Ã8 | 9 | 18 |
| 4ïļâĢ | 11Ã11 | 12 | 24 |
| 5ïļâĢ+ | 14Ã14 | 15 | 30 |
*Para 2 jugadores
ðŊ Fases del Juego
1. ðĢ Fase de ColocaciÃģn (PLACING)
- Cada jugador coloca bombas por turnos
- Las bombas se muestran brevemente (1 segundo) a todos
- ÂĄMemoriza dÃģnde pones TUS bombas y las del rival!
2. ð Fase de BÚsqueda (PLAYING)
- Excava casillas por turnos
- Casilla segura â Se marca en verde â
- Bomba â ÂĄEXPLOSIÃN! Pierdes 1 vida ð
ð Condiciones de Victoria
| CondiciÃģn | Resultado |
|---|---|
| Rival pierde todas las vidas | ÂĄGANASTE! ð |
| Todas las casillas seguras reveladas | ÂĄZona despejada! |
| Superas la ronda 5 | ÂĄVictoria total! ð |
ðĄ Protocolo de ComunicaciÃģn
Mensajes JSON Cliente â Servidor
// Iniciar partida
{ "type": "START_GAME" }
// Colocar bomba (fase PLACING)
{ "type": "PLACE_BOMB", "x": 2, "y": 1 }
// Excavar casilla (fase PLAYING)
{ "type": "CLICK_CELL", "x": 3, "y": 4 }
// Verificar zona despejada
{ "type": "CHECK_DUNGEON_CLEARED" }
Mensajes JSON Servidor â Clientes
// Nueva ronda
{
"type": "NEW_ROUND",
"round": 1,
"grid_size": 3,
"total_bombs_per_player": 3
}
// NotificaciÃģn de turno
{
"type": "TURN_NOTIFY",
"active_player": "('127.0.0.1', 54321)",
"msg": "Turno de ... para poner bombas."
}
// Flash de bomba (visible 1 segundo)
{
"type": "BOMB_flash",
"x": 1, "y": 2,
"who": "('127.0.0.1', 54321)"
}
// ExplosiÃģn
{
"type": "EXPLOSION",
"x": 1, "y": 2,
"who": "...",
"lives": 2
}
// Casilla segura
{ "type": "SAFE", "x": 0, "y": 0 }
// Game Over
{
"type": "GAME_OVER",
"loser": "...",
"msg": "ð ÂĄ... ha perdido todas sus vidas!"
}
ð§ DocumentaciÃģn TÃĐcnica Detallada
ð Sistema de IdentificaciÃģn de Jugadores
El servidor identifica a cada jugador mediante su direcciÃģn de socket, que es una tupla Única (IP, Puerto):
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â IDENTIFICACIÃN ÃNICA DE JUGADORES â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â Cuando un cliente se conecta al servidor: â
â â
â Cliente 1 âââââââš sock.accept() âââââââš ('127.0.0.1', 49956) â
â Cliente 2 âââââââš sock.accept() âââââââš ('127.0.0.1', 49968) â
â â
â Aunque ambos clientes estÃĐn en el MISMO ordenador (127.0.0.1) â
â el PUERTO es diferente y Único para cada conexiÃģn. â
â â
â Esta tupla (IP, Puerto) actÚa como IDENTIFICADOR ÃNICO. â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ÂŋCÃģmo funciona en el mismo ordenador?
# servidor.py:62-65 - Al aceptar conexiÃģn
conn, addr = s.accept() # addr = ('127.0.0.1', 49956)
# El sistema operativo asigna un puerto efÃmero ÃNICO
# a cada nueva conexiÃģn de socket del cliente
| Cliente | IP | Puerto (asignado por SO) | Identificador Completo |
|---|---|---|---|
| Dashboard 1 | 127.0.0.1 | 49956 | ('127.0.0.1', 49956) |
| Dashboard 2 | 127.0.0.1 | 49968 | ('127.0.0.1', 49968) |
| Cliente remoto | 192.168.1.50 | 52341 | ('192.168.1.50', 52341) |
ðĄ Clave: El puerto del cliente es asignado automÃĄticamente por el sistema operativo y es siempre diferente para cada nueva conexiÃģn, incluso desde el mismo ordenador.
ð GestiÃģn de Turnos
El servidor mantiene dos Ãndices de turno para controlar quiÃĐn juega:
# servidor.py - Variables de control de turnos
self.placing_turn_index = 0 # Ãndice en fase PLACING
self.playing_turn_index = 0 # Ãndice en fase PLAYING
self.client_list = [] # Lista ordenada de direcciones
Flujo de VerificaciÃģn de Turno
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â VERIFICACIÃN DE TURNO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â client_list = [('127.0.0.1', 49956), ('127.0.0.1', 49968)] â
â â â â
â Ãndice 0 Ãndice 1 â
â â
â Si placing_turn_index = 0: â
â â Solo ('127.0.0.1', 49956) puede poner bombas â
â â
â Cuando recibe PLACE_BOMB de un cliente: â
â â
â âââââââââââââââââââââââââââââââââââââââââââââââ â
â â if str(addr) != str(current_turn_addr): â â
â â return # No es su turno, ignorar â â
â âââââââââââââââââââââââââââââââââââââââââââââââ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
CÃģdigo de VerificaciÃģn
# servidor.py:222-232 - VerificaciÃģn de turno al colocar bomba
elif msg_type == 'PLACE_BOMB':
if self.state == STATE_PLACING:
# Obtener quiÃĐn tiene el turno actual
current_turn_addr = self.client_list[self.placing_turn_index]
# Comparar con quiÃĐn enviÃģ el mensaje
if str(addr) != str(current_turn_addr):
return # ÂĄNo es tu turno! Ignorar mensaje
# Procesar la bomba...
ðĪ CÃģmo el Cliente Sabe si es su Turno
El cliente guarda su propia direcciÃģn al conectarse y la compara con los mensajes del servidor:
# app.py:1221-1240 - IdentificaciÃģn del cliente y manejo de turno
# Al conectarse
self.my_address = str(sock.getsockname()) # Ej: "('127.0.0.1', 49956)"
# Al recibir TURN_NOTIFY del servidor
def handle_message(self, msg):
if mtype == 'TURN_NOTIFY':
active_player = msg.get('active_player') # "('127.0.0.1', 49956)"
# Comparar con mi direcciÃģn
if active_player == self.my_address:
self._log_game("ðŊ ÂĄES TU TURNO!")
# Habilitar controles...
else:
self._log_game(f"âģ Turno de {active_player}")
# Deshabilitar controles...
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO DE NOTIFICACIÃN DE TURNO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â SERVIDOR CLIENTES â
â ââââââââ ââââââââ â
â â
â broadcast({ Cliente 1 (49956): â
â "type": "TURN_NOTIFY", ââ my_address = 49956 â
â "active_player": ââ active_player = 49956 â
â "('127.0.0.1', 49956)" ââ â ÂĄES MI TURNO! â
â }) â
â â Cliente 2 (49968): â
â ââââââââââââââââââââââââââââš ââ my_address = 49968 â
â ââ active_player = 49956 â
â ââ â No es mi turno â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðĢ DetecciÃģn de Bombas Duplicadas
El servidor usa un Set de Python para almacenar las coordenadas de las bombas, lo que garantiza que no haya duplicados.
Estructura de Datos: self.bombs = set()
# servidor.py:32 - InicializaciÃģn del set de bombas
class GameServer:
def __init__(self):
self.bombs = set() # Set de tuplas (x, y)
Un set() en Python no permite elementos duplicados. Si intentas agregar la misma tupla (x, y) dos veces, solo se guarda una vez.
ValidaciÃģn al Colocar Bomba
# servidor.py:222-246 - ValidaciÃģn completa de colocaciÃģn de bomba
elif msg_type == 'PLACE_BOMB':
if self.state == STATE_PLACING:
# 1. Verificar que es el turno del jugador
current_turn_addr = self.client_list[self.placing_turn_index]
if str(addr) != str(current_turn_addr):
return # â No es su turno, ignorar
x, y = msg['x'], msg['y']
# 2. â VERIFICAR SI YA HAY BOMBA EN ESA CASILLA
if (x, y) in self.bombs:
return # â Ya hay bomba aquÃ, ignorar clic
# 3. â
Agregar bomba al set
self.bombs.add((x, y))
self.current_player_bombs_placed += 1
# 4. Mostrar flash de bomba a todos (1 segundo)
self._broadcast_unlocked({
"type": "BOMB_flash",
"x": x, "y": y,
"who": str(addr)
})
# 5. Si el jugador completÃģ sus bombas, pasar al siguiente
if self.current_player_bombs_placed >= self.bombs_to_place_per_player:
self.placing_turn_index += 1
self.next_placement_turn()
Flujo Completo de ValidaciÃģn
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â VALIDACIÃN DE COLOCACIÃN DE BOMBA â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â Jugador 1 hace clic en casilla (2, 1) â
â â â
â ââââš Mensaje enviado: {"type": "PLACE_BOMB", "x": 2, "y": 1} â
â â â
â ââââš SERVIDOR recibe mensaje â
â â â
â âââš VALIDACIÃN 1: ÂŋEs el turno de este jugador? â
â â ââ current_turn_addr = client_list[placing_index] â
â â ââ if addr != current_turn_addr: return â â
â â â
â âââš VALIDACIÃN 2: ÂŋYa hay bomba en (2, 1)? â
â â ââ if (2, 1) in self.bombs: â
â â â return # â Ignorar clic â
â â ââ self.bombs = {(0,0), (1,1)} â NO contiene (2,1) â
â â â
VÃLIDO â
â â â
â âââš AGREGAR BOMBA: â
â â ââ self.bombs.add((2, 1)) â
â â â self.bombs = {(0,0), (1,1), (2,1)} â
â â â
â âââš BROADCAST a todos: â
â â ââ {"type": "BOMB_flash", "x": 2, "y": 1} â
â â â
â âââš INCREMENTAR contador: â
â ââ current_player_bombs_placed += 1 â
â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
â Jugador 1 hace clic OTRA VEZ en (2, 1) por error â
â â â
â ââââš SERVIDOR recibe: {"type": "PLACE_BOMB", "x": 2, "y": 1} â
â â â
â âââš VALIDACIÃN 1: â
Es su turno â
â â â
â âââš VALIDACIÃN 2: ÂŋYa hay bomba en (2, 1)? â
â â ââ if (2, 1) in self.bombs: â
â â return # â SÃ EXISTE, IGNORAR â
â â â
â âââš ðŦ NO se procesa el clic â
â ðŦ NO se envÃa BOMB_flash â
â ðŦ NO se incrementa contador â
â â
â Resultado: El jugador puede hacer clic mÚltiples veces en la â
â misma casilla, pero solo la primera vez cuenta. â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ÂŋPor quÃĐ usar un Set?
| Estructura | Ventaja | OperaciÃģn in |
|---|---|---|
set() |
â No permite duplicados automÃĄticamente | O(1) - InstantÃĄneo |
list() |
â Permite duplicados, necesita validaciÃģn manual | O(n) - Lento |
dict() |
â Claves Únicas, pero mÃĄs memoria | O(1) - InstantÃĄneo |
# Ejemplo de eficiencia
# â CON LISTA (Lento)
bombs_list = [(0,0), (1,1), (2,2)]
if (2, 1) in bombs_list: # Revisa TODA la lista: O(n)
return
# â
CON SET (RÃĄpido)
bombs_set = {(0,0), (1,1), (2,2)}
if (2, 1) in bombs_set: # Hash lookup instantÃĄneo: O(1)
return
Ejemplo PrÃĄctico: Ronda 1 con 2 Jugadores
Ronda 1: Grid 3Ã3, cada jugador coloca 3 bombas
Estado inicial:
self.bombs = set() # VacÃo
self.bombs_to_place_per_player = 3
âââââââââââââââââââââââââââââââââââââââââ
â TURNO JUGADOR 1 â
âââââââââââââââââââââââââââââââââââââââââĪ
â Clic en (0, 0): â
â ââ (0, 0) in bombs? â NO â
â ââ bombs.add((0, 0)) â
â â bombs = {(0,0)} â
â â
â Clic en (1, 1): â
â ââ (1, 1) in bombs? â NO â
â ââ bombs.add((1, 1)) â
â â bombs = {(0,0), (1,1)} â
â â
â Clic en (0, 0) por error: â
â ââ (0, 0) in bombs? â â
SÃ â
â ââ return (IGNORADO) â â
â â bombs = {(0,0), (1,1)} â
â â contador NO aumenta â
â â
â Clic en (2, 2): â
â ââ (2, 2) in bombs? â NO â
â ââ bombs.add((2, 2)) â
â â bombs = {(0,0), (1,1), (2,2)} â
â â contador = 3 â
â
â â TURNO COMPLETADO â
âââââââââââââââââââââââââââââââââââââââââ
âââââââââââââââââââââââââââââââââââââââââ
â TURNO JUGADOR 2 â
âââââââââââââââââââââââââââââââââââââââââĪ
â Clic en (0, 1): â
â ââ (0, 1) in bombs? â NO â
â ââ bombs.add((0, 1)) â
â â bombs = {(0,0),(1,1),(2,2), â
â (0,1)} â
â â
â Clic en (1, 1) (donde J1 puso): â
â ââ (1, 1) in bombs? â â
SÃ â
â ââ return (IGNORADO) â â
â â NO puede poner bomba encima â
â â
â Clic en (1, 2): â
â ââ bombs.add((1, 2)) â
â â bombs = {..., (1,2)} â
â â
â Clic en (2, 0): â
â ââ bombs.add((2, 0)) â
â â bombs = {(0,0),(1,1),(2,2), â
â (0,1),(1,2),(2,0)} â
â â 6 bombas total (3 por jugador)â
âââââââââââââââââââââââââââââââââââââââââ
Grid final:
0 1 2
âââââŽââââŽââââ
â ðĢâ ðĢâ â 0
âââââžââââžââââĪ
â â ðĢâ ðĢâ 1
âââââžââââžââââĪ
â ðĢâ â ðĢâ 2
âââââīââââīââââ
Total: 6 bombas Únicas, sin duplicados
CÃģdigo del Cliente
# app.py:1310-1330 - Manejo de clic en casilla durante fase de colocaciÃģn
def on_button_click(x, y):
"""Cuando el jugador hace clic en una casilla"""
if self.game_phase == 'PLACING':
# Solo permite clic si es tu turno
if self.my_turn:
# Enviar al servidor
self.game_client.send({"type": "PLACE_BOMB", "x": x, "y": y})
# â ïļ IMPORTANTE: El cliente NO valida duplicados localmente
# El servidor se encarga de toda la validaciÃģn
# Si el servidor ignora el mensaje (duplicado),
# simplemente no recibirÃĄs BOMB_flash
Ventaja de validar en servidor: Un jugador malicioso no puede modificar el cliente para hacer trampas. El servidor tiene la Única fuente de verdad.
ð SincronizaciÃģn con Threading Lock
El servidor usa un threading.Lock para evitar condiciones de carrera cuando mÚltiples clientes envÃan mensajes simultÃĄneamente:
# servidor.py:28-47 - SincronizaciÃģn con Lock
class GameServer:
def __init__(self):
self.lock = threading.Lock() # Mutex para sincronizaciÃģn
self.clients = {} # Diccionario compartido
self.state = STATE_LOBBY # Estado compartido
def process_message(self, client, addr, msg):
with self.lock: # Adquirir lock antes de modificar estado
if msg_type == 'PLACE_BOMB':
# Solo un hilo puede ejecutar esto a la vez
self.bombs.add((x, y))
self._broadcast_unlocked(...)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â SINCRONIZACIÃN CON LOCK â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â Hilo Cliente 1 ââââââ â
â ââââš with self.lock: âââš Ejecuta primero â
â Hilo Cliente 2 ââââââ â â
â âž â
â (espera...) â
â â â
â âž â
â Hilo 2 ejecuta despuÃĐs â
â â
â Esto evita que dos jugadores modifiquen el estado â
â del juego al mismo tiempo (condiciones de carrera). â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðĄ Arquitectura de Hilos
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ARQUITECTURA DE HILOS â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â SERVIDOR (servidor.py) â
â ââââââââââââââââââââââ â
â â
â âââââââââââââââââââ â
â â Hilo Main â âââ Acepta conexiones (s.accept()) â
â ââââââââââŽâââââââââ â
â â â
â ââââš Hilo Cliente 1 âââš handle_client(sock1) â
â ââââš Hilo Cliente 2 âââš handle_client(sock2) â
â ââââš Hilo Cliente N âââš handle_client(sockN) â
â â
â Cada cliente tiene su propio hilo daemon que: â
â 1. Lee mensajes del socket (recv) â
â 2. Procesa el mensaje (process_message) â
â 3. Puede hacer broadcast a todos los clientes â
â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
â CLIENTE (app.py) â
â ââââââââââââââââ â
â â
â âââââââââââââââââââ âââââââââââââââââââ â
â â Hilo Main â â Hilo Recv â â
â â (Tkinter UI) ââââââ (_recv_loop) â â
â âââââââââââââââââââ âââââââââââââââââââ â
â â âē â
â â msg_queue.put() â sock.recv() â
â âž â â
â âââââââââââââââââââ â â
â â Cola de msgs â âââââââââââ â
â â (thread-safe) â â
â âââââââââââââââââââ â
â â
â El hilo de recepciÃģn pone mensajes en una cola. â
â El hilo principal (UI) los procesa con after(). â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðŪ Resumen: Jugar en el Mismo Ordenador
| Paso | QuÃĐ Sucede |
|---|---|
| 1. Iniciar servidor | python servidor.py escucha en puerto 3333 |
| 2. Abrir Cliente 1 | Conecta â SO asigna puerto 49956 |
| 3. Abrir Cliente 2 | Conecta â SO asigna puerto 49968 |
| 4. Servidor registra | clients = {sock1: ('127.0.0.1', 49956), sock2: ('127.0.0.1', 49968)} |
| 5. Iniciar juego | client_list = [addr1, addr2] define orden de turnos |
| 6. Turno jugador 1 | Servidor envÃa TURN_NOTIFY con active_player = addr1 |
| 7. Cliente 1 compara | my_address == active_player â ÂĄEs mi turno! |
| 8. Cliente 2 compara | my_address != active_player â Esperar |
| 9. Jugador 1 actÚa | EnvÃa PLACE_BOMB o CLICK_CELL |
| 10. Servidor valida | addr == current_turn_addr â VÃĄlido, procesar |
| 11. Avanzar turno | placing_turn_index += 1 â Turno del siguiente |
ð ïļ TecnologÃas
|
Python 3 Lenguaje base |
Tkinter Interfaz grÃĄfica |
TCP Sockets ComunicaciÃģn en red |
|
Matplotlib GrÃĄficos |
JSON Protocolo mensajes |
Requests APIs HTTP |
ð§ĩ Conceptos de PSP Aplicados
| Concepto | ImplementaciÃģn |
|---|---|
| Procesos | Lanzamiento de aplicaciones externas (VS Code, Firefox) |
| Threads | Servidor multihilo, cliente con hilos de recepciÃģn |
| Sockets TCP | ComunicaciÃģn cliente-servidor en red (Juego y Correo) |
| Servicios | API OpenWeather, scraping de Wallapop, IMAP/SMTP |
| SincronizaciÃģn | Locks para acceso concurrente a estado compartido |
ð§ CLIENTE DE CORREO ELECTRÃNICO: ARQUITECTURA Y FUNCIONAMIENTO
ðïļ Arquitectura del Sistema de Correo
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ARQUITECTURA DEL CLIENTE DE CORREO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
âââââââââââââââââââââââââââââ
â SERVIDOR WEBMIN â
â 10.10.0.101 â
âââââââââââââââŽââââââââââââââ
â
âââââââââââââââââââââââīââââââââââââââââââââââ
â â
âââââââââžâââââââââ ââââââââââžâââââââââ
â PUERTO 143 â â PUERTO 25 â
â IMAP â â SMTP â
â (Lectura) â â (EnvÃo) â
âââââââââŽâââââââââ ââââââââââŽâââââââââ
â â
âââââââââââââââââââŽââââââââââââââââââââââââ
â
âââââââââââžâââââââââââ
â app.py â
â DashboardApp â
â â
â Tab "Correos" â
ââââââââââââââââââââââ
â
âââââââââââââââââââââââžââââââââââââââââââââââ
â â â
âââââââââžâââââââââ âââââââââââžâââââââââ ââââââââââžâââââââââ
â _connect_ â â _refresh_ â â _send_mail_ â
â mail_server() â â mail_list() â â with_attach() â
â â â â â â
â IMAP Login â â IMAP FETCH â â SMTP Send â
ââââââââââââââââââ ââââââââââââââââââââ âââââââââââââââââââ
ðĄ Protocolos Utilizados
IMAP (Internet Message Access Protocol)
Protocolo para leer correos del servidor. Puerto: 143 (sin TLS).
# app.py:1578-1582 - ConexiÃģn IMAP
import imaplib
self.imap_connection = imaplib.IMAP4(host, port_num) # Puerto 143
self.imap_connection.login(username, password)
Comandos IMAP utilizados:
login(user, pass)â AutenticaciÃģnselect(mailbox)â Seleccionar carpeta (INBOX, Sent, etc.)search(None, 'ALL')â Buscar todos los correosfetch(id, '(BODY.PEEK[] FLAGS)')â Obtener correo sin marcarlo como leÃdostore(id, '+FLAGS', '\\Seen')â Marcar como leÃdoappend(folder, flags, date, msg)â Guardar correo en carpeta
SMTP (Simple Mail Transfer Protocol)
Protocolo para enviar correos. Puerto: 25 (sin TLS).
# app.py:2949-2955 - ConexiÃģn y envÃo SMTP
import smtplib
with smtplib.SMTP(smtp_host, smtp_port_num, timeout=15) as server:
server.send_message(msg, to_addrs=recipients)
ð Sistema de Credenciales
Guardado AutomÃĄtico con Base64
# app.py:1540-1557 - FunciÃģn _save_mail_credentials()
import base64
config = {
'imap_host': '10.10.0.101',
'imap_port': '143',
'smtp_host': '10.10.0.101',
'smtp_port': '25',
'username': 'marcos@psp.es',
'password': base64.b64encode(password.encode()).decode() # Codificar
}
json.dump(config, open('.mail_config.json', 'w'), indent=2)
# app.py:1512-1520 - FunciÃģn _load_mail_credentials()
config = json.load(open('.mail_config.json', 'r'))
password = base64.b64decode(config['password']).decode() # Decodificar
Flujo de guardado:
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO DE GUARDADO DE CREDENCIALES â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â 1. Usuario ingresa credenciales â
â âââš Usuario: marcos@psp.es â
â âââš Password: 1234 â
â â
â 2. Marca checkbox "ðū Recordar credenciales" â
â âââš mail_remember_var.get() = True â
â â
â 3. Al conectar exitosamente, se llama _save_mail_credentials() â
â â
â 4. Base64 encoding: â
â Password "1234" â Bytes b'1234' â
â â Base64 b'MTIzNA==' â
â â String "MTIzNA==" â
â â
â 5. Se guarda en .mail_config.json: â
â { â
â "username": "marcos@psp.es", â
â "password": "MTIzNA==" â
â } â
â â
â 6. Al reiniciar app.py: â
â âââš _load_mail_credentials() lee el archivo â
â âââš Decodifica Base64: "MTIzNA==" â "1234" â
â âââš Precarga los campos automÃĄticamente â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ïļ IMPORTANTE: Base64 NO es encriptaciÃģn, solo ofuscaciÃģn. El archivo .mail_config.json estÃĄ protegido en .gitignore para no subirlo a Git.
ðŽ Lectura de Correos (IMAP)
FunciÃģn Principal: _refresh_mail_list() (lÃneas 1635-1801)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO DE LECTURA DE CORREOS â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â 1. SELECT 'INBOX' â
â âââš imap_connection.select('INBOX') â
â â
â 2. SEARCH ALL â
â âââš status, messages = imap_connection.search(None, 'ALL') â
â âââš mail_ids = messages[0].split() # [b'1', b'2', b'3', ...] â
â â
â 3. FETCH con BODY.PEEK[] (no marca como leÃdo) â
â for mail_id in mail_ids: â
â status, msg_data = imap_connection.fetch( â
â mail_id, â
â '(BODY.PEEK[] FLAGS)' # â PEEK es clave â
â ) â
â â
â 4. EXTRAER FLAGS (\Seen) â
â âââš Busca en respuesta IMAP: "FLAGS (\\Seen ...)" â
â âââš is_seen = True si contiene "\\Seen" â
â â
â 5. PARSEAR EMAIL â
â âââš msg = email.message_from_bytes(msg_data) â
â âââš from_addr = decode_header(msg['From'])[0][0] â
â âââš subject = decode_header(msg['Subject'])[0][0] â
â â
â 6. GUARDAR EN LISTA LOCAL â
â self.mail_list.append({ â
â 'id': mail_id, â
â 'from': from_addr, â
â 'subject': subject, â
â 'date': date_str, â
â 'msg': msg, # Objeto email completo â
â 'is_seen': is_seen â
â }) â
â â
â 7. MOSTRAR EN LISTBOX â
â if is_seen: â
â display = f' {from_addr} - {subject}' # Sin emoji â
â itemconfig(idx, fg='#888888') # Gris â
â else: â
â display = f'ðĩ {from_addr} - {subject}' # Con emoji â
â itemconfig(idx, fg='#000000') # Negro â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ÂŋPor quÃĐ BODY.PEEK[]?
# â MAL: RFC822 marca el correo como leÃdo automÃĄticamente
fetch(mail_id, 'RFC822')
# â
BIEN: BODY.PEEK[] lee sin cambiar el flag \Seen
fetch(mail_id, '(BODY.PEEK[] FLAGS)')
ðĪ EnvÃo de Correos (SMTP)
FunciÃģn: _send_mail_with_attachments() (lÃneas 2837-2977)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO DE ENVÃO DE CORREO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â 1. VALIDAR DESTINATARIOS â
â to_addr_raw = "marcos@psp.es, user2@example.com" â
â âââš Split por comas/punto y coma â
â âââš recipients = ['marcos@psp.es', 'user2@example.com'] â
â âââš Validar regex: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,} â
â â
â 2. CREAR MENSAJE MIME MULTIPART â
â from email.mime.multipart import MIMEMultipart â
â msg = MIMEMultipart() â
â msg['From'] = 'marcos@psp.es' â
â msg['To'] = 'marcos@psp.es, user2@example.com' â
â msg['Subject'] = 'Asunto del correo' â
â â
â 3. ADJUNTAR CUERPO â
â from email.mime.text import MIMEText â
â msg.attach(MIMEText(body, 'plain', 'utf-8')) â
â â
â 4. ADJUNTAR ARCHIVOS â
â for file_path in attachments: â
â if file_ext == '.png': â
â part = MIMEImage(file_data, name=filename) â
â elif file_ext == '.pdf': â
â part = MIMEApplication(file_data, _subtype='pdf') â
â # ... otros tipos â
â msg.attach(part) â
â â
â 5. CONECTAR SMTP Y ENVIAR â
â with smtplib.SMTP('10.10.0.101', 25) as server: â
â server.send_message(msg, to_addrs=recipients) â
â â
â 6. GUARDAR EN CARPETA SENT (IMAP) â
â âââš _save_to_sent_folder(msg) # Ver siguiente secciÃģn â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tipos MIME Soportados
| ExtensiÃģn | Tipo MIME | Clase Python |
|---|---|---|
.png, .jpg |
image/* |
MIMEImage |
.pdf |
application/pdf |
MIMEApplication |
.doc, .docx |
application/msword |
MIMEApplication |
.xls, .xlsx |
application/vnd.ms-excel |
MIMEApplication |
.zip, .rar |
application/zip |
MIMEApplication |
.txt |
text/plain |
MIMEText |
| Otros | application/octet-stream |
MIMEBase |
ðū Guardado en Servidor IMAP
FunciÃģn: _save_to_sent_folder() (lÃneas 2979-3033)
DespuÃĐs de enviar un correo por SMTP, se guarda una copia en la carpeta "Sent" del servidor IMAP para que aparezca en Webmin y otros clientes.
# app.py:2990-3015 - Guardar correo en carpeta Sent del servidor
# Intentar mÚltiples nombres de carpeta
sent_folders = ['Sent', 'INBOX.Sent', 'Enviados', 'INBOX.Enviados', 'Sent Items']
for folder in sent_folders:
try:
# Intentar seleccionar la carpeta
status, _ = self.imap_connection.select(folder)
if status == 'OK':
# Guardar correo con APPEND
self.imap_connection.append(
folder,
'\\Seen', # Marcar como leÃdo
imaplib.Time2Internaldate(time.time()),
msg.as_bytes()
)
break
except:
continue
# Si no existe, crearla
if not folder_found:
self.imap_connection.create('Sent')
self.imap_connection.append('Sent', '\\Seen', date, msg_bytes)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â GUARDADO EN CARPETA SENT DEL SERVIDOR â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â 1. Usuario envÃa correo con SMTP â
â âââš Correo enviado a marcos@psp.es â
â â
â 2. _save_to_sent_folder() se ejecuta automÃĄticamente â
â â
â 3. Intenta conectar a carpeta "Sent": â
â âââš Intenta: SELECT 'Sent' â Falla â
â âââš Intenta: SELECT 'INBOX.Sent' â
OK â
â âââš Carpeta encontrada â
â â
â 4. Guarda el correo con APPEND: â
â APPEND "INBOX.Sent" (\Seen) "19-Feb-2026 19:30:00" {bytes} â
â â
â 5. Resultado: â
â âââââââââââââââââââââââââââââââââââââââ â
â â SERVIDOR WEBMIN â â
â â ââ INBOX (3 correos) â â
â â ââ Sent (1 correo NUEVO) â AQUÃ â â
â âââââââââââââââââââââââââââââââââââââââ â
â â
â 6. Al abrir Webmin: â
â âââš El correo aparece en "Sent" â
â âââš Otros clientes (Thunderbird, etc.) tambiÃĐn lo ven â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðĨ EnvÃo a MÚltiples Destinatarios
ValidaciÃģn y Parsing
# app.py:2703-2740 - ValidaciÃģn de mÚltiples destinatarios
# Entrada del usuario
to_addr_raw = "marcos@psp.es, user2@example.com; user3@test.org"
# 1. Split por comas O punto y coma
import re
recipients = re.split(r'[;,]\s*', to_addr_raw)
# â ['marcos@psp.es', 'user2@example.com', 'user3@test.org']
# 2. Limpiar espacios
recipients = [r.strip() for r in recipients if r.strip()]
# 3. Validar formato con regex
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
invalid_emails = [email for email in recipients if not re.match(email_pattern, email)]
if invalid_emails:
messagebox.showwarning('â ïļ Advertencia',
f'Los siguientes emails no son vÃĄlidos:\n{", ".join(invalid_emails)}')
return
# 4. Confirmar si son mÚltiples
if len(recipients) > 1:
confirm = messagebox.askyesno('ð§ MÚltiples destinatarios',
f'ÂŋEnviar correo a {len(recipients)} destinatarios?\n\n' +
'\n'.join(f' âĒ {email}' for email in recipients))
if not confirm:
return
# 5. Enviar a todos
self._send_mail_with_attachments(recipients, subject, body, attachments)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ENVÃO A MÃLTIPLES DESTINATARIOS â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â Usuario escribe en campo "Para:": â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â marcos@psp.es, user2@test.com, user3@example.org â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
â Al hacer clic en "ðĪ ENVIAR": â
â â
â 1. Parse: Split por ',' o ';' â
â ['marcos@psp.es', 'user2@test.com', 'user3@example.org'] â
â â
â 2. ValidaciÃģn regex de cada email â
â â
marcos@psp.es â VÃĄlido â
â â
user2@test.com â VÃĄlido â
â â
user3@example.org â VÃĄlido â
â â
â 3. DiÃĄlogo de confirmaciÃģn: â
â âââââââââââââââââââââââââââââââââââââ â
â â ÂŋEnviar a 3 destinatarios? â â
â â â â
â â âĒ marcos@psp.es â â
â â âĒ user2@test.com â â
â â âĒ user3@example.org â â
â â â â
â â [SÃ] [No] â â
â âââââââââââââââââââââââââââââââââââââ â
â â
â 4. SMTP envÃa a todos: â
â msg['To'] = 'marcos@psp.es, user2@test.com, user3@example.org' â
â server.send_message(msg, to_addrs=[...]) â
â â
â 5. Mensaje de ÃĐxito: â
â â
"Correo enviado correctamente a 3 destinatarios" â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ð Manejo de Adjuntos
Adjuntar Archivos
# app.py:2568-2590 - Adjuntar archivos con diÃĄlogo
# Usuario hace clic en "ð Adjuntar archivo"
file_paths = filedialog.askopenfilenames(
title='Seleccionar archivos',
filetypes=[
('ImÃĄgenes', '*.png *.jpg *.jpeg *.gif'),
('PDFs', '*.pdf'),
('Documentos', '*.doc *.docx *.xls *.xlsx'),
('Todos', '*.*')
]
)
# Se guardan en lista
attachments.append(file_path)
# Al enviar, se procesan (app.py:2895-2920):
for file_path in attachments:
file_name = os.path.basename(file_path) # "documento.pdf"
file_ext = os.path.splitext(file_path)[1] # ".pdf"
with open(file_path, 'rb') as f:
file_data = f.read()
if file_ext == '.pdf':
part = MIMEApplication(file_data, _subtype='pdf')
part.add_header('Content-Disposition', 'attachment', filename=file_name)
msg.attach(part)
ImÃĄgenes Inline con Ctrl+V
# app.py:2609-2645 - Pegar imagen desde portapapeles
# Usuario copia una imagen y presiona Ctrl+V
def on_paste(event):
try:
# Obtener imagen del portapapeles
img = ImageGrab.grabclipboard()
if img:
# Guardar temporalmente
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
img.save(temp_file.name)
# Mostrar miniatura en interfaz
thumbnail = img.resize((150, 150))
photo = ImageTk.PhotoImage(thumbnail)
label = tk.Label(frame, image=photo)
label.image = photo # Mantener referencia
label.pack()
# Agregar a lista de adjuntos
inline_images_data.append({'data': img_bytes})
except:
pass
ðžïļ VisualizaciÃģn de Correos
FunciÃģn: _display_mail() (lÃneas 2055-2337)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â VISUALIZACIÃN DE CORREO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â 1. Usuario hace clic en un correo de la lista â
â âââš _on_mail_select() â _display_mail(mail_info) â
â â
â 2. Marcar como leÃdo en servidor (si es INBOX y no leÃdo) â
â if not mail_info['is_seen']: â
â imap_connection.store(mail_id, '+FLAGS', '\\Seen') â
â # Actualizar visualmente: quitar ðĩ, poner gris â
â â
â 3. Actualizar encabezados â
â mail_from_label.config(text='De: marcos@psp.es') â
â mail_subject_label.config(text='Asunto: Test') â
â mail_date_label.config(text='Fecha: 19/02/2026') â
â â
â 4. Procesar contenido multipart â
â if msg.is_multipart(): â
â for part in msg.walk(): â
â if content_type == 'text/plain': â
â body = part.get_payload(decode=True).decode() â
â elif part.get_filename(): # Adjunto â
â attachments.append({...}) â
â â
â 5. Mostrar cuerpo en Text widget â
â mail_body_text.delete('1.0', 'end') â
â mail_body_text.insert('1.0', body) â
â â
â 6. Mostrar imÃĄgenes inline (si PIL estÃĄ disponible) â
â for att in images: â
â img = Image.open(BytesIO(att['data'])) â
â img.thumbnail((500, 500)) # Redimensionar â
â photo = ImageTk.PhotoImage(img) â
â mail_body_text.image_create('end', image=photo) â
â â
â 7. Mostrar otros adjuntos (PDFs, docs, etc.) â
â for att in other_attachments: â
â # Frame con icono, nombre, tamaÃąo y botÃģn "ðū Guardar" â
â icon = 'ð' if PDF else 'ð' if Word else 'ð' â
â Button(text='ðū Guardar', command=save_file) â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðĩ Sistema de Indicadores Visuales
# app.py:1775-1790 - Indicadores visuales de correos leÃdos/no leÃdos
# Al cargar correos
for mail in mail_list:
if is_seen:
# Correo leÃdo
display_text = f' {from_addr[:27]} - {subject[:37]}'
self.mail_listbox.insert('end', display_text)
idx = self.mail_listbox.size() - 1
self.mail_listbox.itemconfig(idx,
fg='#888888', # Gris
selectforeground='#666666'
)
else:
# Correo NO leÃdo
display_text = f'ðĩ {from_addr[:27]} - {subject[:37]}'
self.mail_listbox.insert('end', display_text)
idx = self.mail_listbox.size() - 1
self.mail_listbox.itemconfig(idx,
fg='#000000', # Negro
selectforeground='#1a73e8'
)
# Contador de no leÃdos
self.unread_count = sum(1 for m in self.mail_list if not m.get('is_seen', False))
self.unread_label.config(text=f'Correos sin leer: {self.unread_count}')
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â APARIENCIA VISUAL EN LISTBOX â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â ðŽ Bandeja de entrada Correos sin leer: 2 â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ â
â â â â
â â ðĩ marcos@psp.es - Test para grabaciÃģn â NO LEÃDO (negro) â â
â â ðĩ user@example.com - Propuesta proyecto â NO LEÃDO (negro) â â
â â admin@server.com - NotificaciÃģn â LEÃDO (gris) â â
â â webmaster@test.org - Informe â LEÃDO (gris) â â
â â â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
â Al hacer clic en el primero (ðĩ NO LEÃDO): â
â 1. Se marca como leÃdo en el servidor (STORE +FLAGS \Seen) â
â 2. Se actualiza la visualizaciÃģn: â
â ââ Quita el emoji ðĩ â
â ââ Cambia color a gris â
â ââ Decrementa contador: "Correos sin leer: 1" â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðĄïļ Manejo de Errores y Log
# app.py:4304-4315 - FunciÃģn _log() con verificaciÃģn de widget
def _log(self, text: str) -> None:
# Verificar si estamos en hilo principal
if threading.current_thread() is not threading.main_thread():
self.after(0, lambda t=text: self._log(t))
return
# Verificar si el widget notes existe
if not hasattr(self, 'notes') or self.notes is None:
print(f'[LOG] {text}') # Consola durante inicializaciÃģn
return
# Log normal en interfaz
timestamp = datetime.datetime.now().strftime('%H:%M:%S')
self.notes.insert('end', f'[{timestamp}] {text}\n')
self.notes.see('end')
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â EJEMPLO DE LOG EN INTERFAZ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â ð Panel de Notas â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ â
â â [19:28:43] Conectando a 10.10.0.101:143... â â
â â [19:28:44] ConexiÃģn IMAP establecida â â
â â [19:28:44] Carpetas IMAP disponibles: ['INBOX', 'Sent'] â â
â â [19:28:44] Credenciales guardadas correctamente â â
â â [19:28:45] Cargando 8 correos... â â
â â [19:28:46] 8 correos cargados (2 sin leer) â â
â â [19:29:10] === CORREO SELECCIONADO #0: Test grabaciÃģn === â â
â â [19:29:10] Correo marcado como leÃdo en el servidor â â
â â [19:29:10] >>> Actualizando encabezados â â
â â [19:29:10] Texto plano encontrado: 245 caracteres â â
â â [19:29:10] Adjunto detectado: imagen.png (image/png) â â
â â [19:29:10] Imagen inline mostrada: imagen.png â â
â â [19:29:10] >>> _display_mail COMPLETADO OK â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ð Resumen de Funciones Clave
| FunciÃģn | LÃneas | Responsabilidad |
|---|---|---|
_build_tab_correos() |
632-850 | Construir toda la interfaz del tab Correos |
_load_mail_credentials() |
1483-1525 | Cargar credenciales de .mail_config.json |
_save_mail_credentials() |
1527-1559 | Guardar credenciales con Base64 |
_connect_mail_server() |
1561-1613 | Conectar a IMAP, listar carpetas |
_refresh_mail_list() |
1635-1801 | Cargar correos de INBOX con FETCH |
_show_inbox() |
1803-1811 | Cambiar a bandeja de entrada |
_show_sent() |
1828-1880 | Cambiar a carpeta de enviados |
_on_mail_select() |
1982-2053 | Manejar clic en correo, marcar como leÃdo |
_display_mail() |
2055-2337 | Mostrar contenido, imÃĄgenes y adjuntos |
_open_compose_window() |
2353-2788 | Abrir ventana de redacciÃģn |
_send_mail_with_attachments() |
2837-2977 | Enviar correo por SMTP con adjuntos |
_save_to_sent_folder() |
2979-3033 | Guardar copia en servidor IMAP |
ð Flujo Completo de Usuario
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO COMPLETO: LEER Y ENVIAR CORREO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â 1. INICIAR APLICACIÃN â
â âââš python3 app.py â
â âââš _load_mail_credentials() precarga usuario y contraseÃąa â
â â
â 2. IR A TAB "CORREOS" â
â âââš _build_tab_correos() ya construyÃģ la interfaz â
â â
â 3. CONECTAR â
â âââš Clic en "ð Conectar" â
â âââš _connect_mail_server() â
â ââ IMAP4('10.10.0.101', 143) â
â ââ login('marcos@psp.es', '1234') â
â ââ list() â Muestra carpetas disponibles â
â âââš _save_mail_credentials() si "Recordar" estÃĄ marcado â
â âââš _refresh_mail_list() carga correos automÃĄticamente â
â â
â 4. LEER CORREO â
â âââš Clic en correo de la lista â
â âââš _on_mail_select() â
â ââ store(id, '+FLAGS', '\\Seen') si no leÃdo â
â ââ _display_mail(mail_info) â
â ââ Actualiza encabezados â
â ââ Muestra cuerpo â
â ââ Muestra imÃĄgenes inline â
â ââ Muestra botones para guardar adjuntos â
â â
â 5. ENVIAR NUEVO CORREO â
â âââš Clic en "âïļ Nuevo correo" â
â âââš _open_compose_window() â
â ââ Ventana emergente con campos â
â ââ BotÃģn "ð Adjuntar archivo" â
â ââ Soporte Ctrl+V para imÃĄgenes â
â ââ BotÃģn "ðĪ ENVIAR CORREO" â
â ââ Validar destinatarios (regex) â
â ââ Confirmar si mÚltiples â
â ââ _send_mail_with_attachments() â
â â ââ Crear MIMEMultipart â
â â ââ Adjuntar archivos â
â â ââ SMTP send_message() â
â ââ _save_to_sent_folder() â
â ââ IMAP APPEND a 'Sent' â
â â
â 6. VERIFICAR EN WEBMIN â
â âââš Abrir http://10.10.0.101:20000 â
â âââš Carpeta "Sent" â Correo aparece ahà â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ðĻ CaracterÃsticas del Dashboard
ð Monitor del Sistema
- GrÃĄfico de CPU en lÃnea temporal
- GrÃĄfico de memoria como ÃĄrea
- Contador de hilos del proceso
ðĪïļ API del Tiempo (JÃĄvea)
- Temperatura actual y sensaciÃģn tÃĐrmica
- Humedad y velocidad del viento
- DescripciÃģn del clima
ð AnÃĄlisis Wallapop
- ExtracciÃģn de informaciÃģn de anuncios
- Headers personalizados para API
- Resultados formateados
â° Sistema de Alarmas
- ProgramaciÃģn en minutos
- Notificaciones visuales
- GestiÃģn de alarmas activas
ðĪ Contribuir
- Fork del proyecto
- Crea tu Feature Branch (
git checkout -b feature/NuevaFuncion) - Commit tus cambios (
git commit -m 'Add: Nueva funciÃģn') - Push a la rama (
git push origin feature/NuevaFuncion) - Abre un Pull Request
ð Licencia
Este proyecto es de carÃĄcter educativo y fue desarrollado como parte del mÃģdulo de ProgramaciÃģn de Servicios y Procesos.
Desarrollado con âĪïļ por Marcos Ferrandiz
Proyecto 1š EvaluaciÃģn - PSP (ProgramaciÃģn de Servicios y Procesos)