Proyecto 1 AVA psp
Go to file
marcos f1906ec18e correo 2026-02-19 20:02:35 +01:00
.gitignore correo 2026-02-19 20:02:35 +01:00
ARQUITECTURA_CORREO.md correo 2026-02-19 20:02:35 +01:00
CORREO_README.md correo 2026-02-19 20:02:35 +01:00
README.md correo 2026-02-19 20:02:35 +01:00
TESTING_GUIDE.md correo 2026-02-19 20:02:35 +01:00
app.py correo 2026-02-19 20:02:35 +01:00
cliente_juego.py feat: Implement multiplayer Minesweeper game server with JSON protocol, game states, and turn-based logic, alongside a new game client. 2026-02-02 17:22:11 +01:00
requirements.txt correo 2026-02-19 20:02:35 +01:00
servidor.py feat: Implement multiplayer Minesweeper game server with JSON protocol, game states, and turn-based logic, alongside a new game client. 2026-02-02 17:22:11 +01:00
test_mail_server.py correo 2026-02-19 20:02:35 +01:00
test_startup.py correo 2026-02-19 20:02:35 +01:00

README.md

Python Tkinter Sockets License

ðŸ’Ģ 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

  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

// 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 - 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)

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 - 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:

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
Python 3
Lenguaje base
Tkinter
Tkinter
Interfaz grÃĄfica
Sockets
TCP Sockets
ComunicaciÃģn en red
Matplotlib
Matplotlib
GrÃĄficos
JSON
JSON
Protocolo mensajes
Requests
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).

# 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).

# 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

# 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)
# 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[]?

# ❌ 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.

# 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)

# 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

# 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

# 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

# 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

# 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)


Estado VersiÃģn Python