Proyecto1AVApsp/README.md

1412 lines
76 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<img src="https://img.shields.io/badge/Python-3.8+-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python">
<img src="https://img.shields.io/badge/Tkinter-GUI-green?style=for-the-badge&logo=python&logoColor=white" alt="Tkinter">
<img src="https://img.shields.io/badge/TCP/IP-Multiplayer-orange?style=for-the-badge&logo=socketdotio&logoColor=white" alt="Sockets">
<img src="https://img.shields.io/badge/License-Educational-purple?style=for-the-badge" alt="License">
</p>
<h1 align="center">💣 Minesweeper Multiplayer + Dashboard</h1>
<p align="center">
<strong>Un proyecto completo de programación de servicios y procesos</strong><br>
<em>Juego de buscaminas competitivo en red + Panel de control integral</em>
</p>
<p align="center">
<a href="#-características">Características</a>
<a href="#-arquitectura">Arquitectura</a>
<a href="#-instalación">Instalación</a>
<a href="#-uso">Uso</a>
<a href="#-mecánicas-del-juego">Mecánicas</a>
<a href="#-tecnologías">Tecnologías</a>
</p>
---
## 📸 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
```
<details>
<summary>📦 <strong>Ver dependencias detalladas</strong></summary>
| 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) |
</details>
---
## 🎯 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
<table>
<tr>
<td align="center" width="150">
<img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-original.svg" width="48" height="48" alt="Python" />
<br><strong>Python 3</strong>
<br><em>Lenguaje base</em>
</td>
<td align="center" width="150">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Virtualbox_logo.png/64px-Virtualbox_logo.png" width="48" height="48" alt="Tkinter" />
<br><strong>Tkinter</strong>
<br><em>Interfaz gráfica</em>
</td>
<td align="center" width="150">
<img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/socketio/socketio-original.svg" width="48" height="48" alt="Sockets" />
<br><strong>TCP Sockets</strong>
<br><em>Comunicación en red</em>
</td>
</tr>
<tr>
<td align="center">
<img src="https://matplotlib.org/stable/_static/logo_light.svg" width="48" height="48" alt="Matplotlib" />
<br><strong>Matplotlib</strong>
<br><em>Gráficos</em>
</td>
<td align="center">
<img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/json/json-original.svg" width="48" height="48" alt="JSON" />
<br><strong>JSON</strong>
<br><em>Protocolo mensajes</em>
</td>
<td align="center">
<img src="https://www.vectorlogo.zone/logos/pocoo_flask/pocoo_flask-icon.svg" width="48" height="48" alt="Requests" />
<br><strong>Requests</strong>
<br><em>APIs HTTP</em>
</td>
</tr>
</table>
### 🧵 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**.
---
<p align="center">
<strong>Desarrollado con ❤️ por Marcos Ferrandiz</strong>
</p>
<p align="center">
<em>Proyecto 1º Evaluación - PSP (Programación de Servicios y Procesos)</em>
</p>
---
<p align="center">
<img src="https://img.shields.io/badge/Estado-Completado-success?style=flat-square" alt="Estado">
<img src="https://img.shields.io/badge/Versión-1.0-blue?style=flat-square" alt="Versión">
<img src="https://img.shields.io/badge/Made%20with-Python-yellow?style=flat-square&logo=python" alt="Python">
</p>