๐ฃ 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
```bash
git clone https://github.com/MarcosFerrandiz/Proyecto1AVApsp.git
cd Proyecto1AVApsp
```
### Paso 2: Crear Entorno Virtual (Recomendado)
```bash
python -m venv .venv
# Linux/macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
```
### Paso 3: Instalar Dependencias
```bash
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
```bash
python servidor.py
```
> El servidor escucharรก en `0.0.0.0:3333`
### ๐ฎ Opciรณn A: Dashboard Completo
```bash
python app.py
```
Incluye el juego integrado + todas las funcionalidades del panel.
### ๐ฎ Opciรณn B: Cliente Standalone
```bash
python cliente_juego.py
```
Cliente ligero solo para jugar al Minesweeper.
### ๐ Conectar al Juego
1. Introduce el **Host** del servidor (por defecto: `127.0.0.1`)
2. Verifica el **Puerto** (por defecto: `3333`)
3. Pulsa **"Conectar"**
4. ยก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
```javascript
// 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
```javascript
// 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?
```python
# servidor.py - 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:
```python
# 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)
```python
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:
```python
# app.py - 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 โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
---
### ๐ 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:
```python
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).
```python
# Conexiรณn IMAP (app.py lรญneas 1578-1582)
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รณn
- `select(mailbox)` โ Seleccionar carpeta (INBOX, Sent, etc.)
- `search(None, 'ALL')` โ Buscar todos los correos
- `fetch(id, '(BODY.PEEK[] FLAGS)')` โ Obtener correo sin marcarlo como leรญdo
- `store(id, '+FLAGS', '\\Seen')` โ Marcar como leรญdo
- `append(folder, flags, date, msg)` โ Guardar correo en carpeta
#### **SMTP (Simple Mail Transfer Protocol)**
Protocolo para **enviar correos**. Puerto: **25** (sin TLS).
```python
# Conexiรณn SMTP (app.py lรญneas 2949-2955)
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**
```python
# GUARDAR (app.py lรญneas 1540-1557)
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)
```
```python
# CARGAR (app.py lรญneas 1512-1520)
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[]?**
```python
# โ 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.
```python
# 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 (lรญneas 2703-2740)**
```python
# 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**
```python
# 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:
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**
```python
# 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
```python
# Al cargar correos (lรญneas 1775-1790)
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
```python
# Funciรณn de log (lรญneas 4304-4315)
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
1. **Fork** del proyecto
2. Crea tu **Feature Branch** (`git checkout -b feature/NuevaFuncion`)
3. **Commit** tus cambios (`git commit -m 'Add: Nueva funciรณn'`)
4. **Push** a la rama (`git push origin feature/NuevaFuncion`)
5. 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)
---