|
|
||
|---|---|---|
| .gitignore | ||
| ARQUITECTURA_CORREO.md | ||
| CORREO_README.md | ||
| README.md | ||
| TESTING_GUIDE.md | ||
| app.py | ||
| cliente_juego.py | ||
| requirements.txt | ||
| servidor.py | ||
| test_mail_server.py | ||
| test_startup.py | ||
README.md
ðĢ Minesweeper Multiplayer + Dashboard
Un proyecto completo de programaciÃģn de servicios y procesos
Juego de buscaminas competitivo en red + Panel de control integral
CaracterÃsticas âĒ Arquitectura âĒ InstalaciÃģn âĒ Uso âĒ MecÃĄnicas âĒ TecnologÃas
ðļ Vista Previa
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ðŪ MINESWEEPER MULTIPLAYER DASHBOARD â
ââââââââââââââââŽâââââââââââââââââââââââââââââââââââââââŽââââââââââââââââââââââââĪ
â â â âââââââââââââââââââ â
â ACCIONES â ÃREA DE RESULTADOS â â ðĢ MINESWEEPER â â
â ââââââââââ â â â MULTIPLAYER â â
â â ââââââââââââââââââââââââââââââ â âââââââââââââââââââĪ â
â > Wallapop â â ð Monitor Sistema â â â Ronda: 3 â â
â > Scraping â â ð CPU: 45% â â â ðĢ Bombas: 9 â â
â > API Tiempo â â ðū RAM: 2.1GB â â â âĪïļ Vidas: 2 â â
â â ââââââââââââââââââââââââââââââ â âââââââââââââââââââĪ â
â APPS â â â âââââŽââââŽââââ â â
â ââââââââââ â Tabs: [Resultados][Navegador] â â â âĒ â âĒ â â â â â
â > VS Code â [Correos][Bloc][Tareas] â â âââââžââââžââââĪ â â
â > Camellos â [Alarmas][Enlaces] â â â ðĨâ âĒ â âĒ â â â
â â â â âââââīââââīââââ â â
â BATCH â ââââââââââââââââââââââââââââââ â âââââââââââââââââââĪ â
â ââââââââââ â â ð Panel de Notas â â â [Iniciar Juego] â â
â > Backups â â â â â [Zona Despejada]â â
â â ââââââââââââââââââââââââââââââ â âââââââââââââââââââ â
ââââââââââââââââīâââââââââââââââââââââââââââââââââââââââīââââââââââââââââââââââââ
âĻ CaracterÃsticas
ðŪ Juego Minesweeper Multijugador
| CaracterÃstica | DescripciÃģn |
|---|---|
| ðĨ Competitivo | 2+ jugadores compiten en tiempo real |
| ðĢ ColocaciÃģn estratÃĐgica | Cada jugador coloca bombas para el rival |
| ð Por turnos | Sistema de turnos para colocar y buscar |
| ð Dificultad progresiva | 5 rondas con grids y bombas crecientes |
| âĪïļ Sistema de vidas | 3 vidas por partida, ÂĄno las pierdas! |
ð Dashboard Integral
- ðĄ Monitor del sistema en tiempo real (CPU, RAM, hilos)
- ðĪïļ API del tiempo para JÃĄvea (OpenWeather)
- ð AnÃĄlisis de Wallapop - Scraping de anuncios
- â° Sistema de alarmas programables
- ð Bloc de notas integrado
- ð Gestor de enlaces rÃĄpidos
- ðē Minijuego de camellos con animaciones
ð§ Cliente de Correo ElectrÃģnico Completo
| CaracterÃstica | DescripciÃģn |
|---|---|
| ðŽ IMAP | Lectura de correos desde servidor Webmin (Puerto 143) |
| ðĪ SMTP | EnvÃo de correos con autenticaciÃģn (Puerto 25) |
| ðū Auto-guardado | Credenciales guardadas con Base64 |
| ðĨ MÚltiples destinatarios | EnvÃo a varios correos simultÃĄneamente |
| ð Adjuntos | Soporte para imÃĄgenes, PDFs, docs, Excel, ZIP |
| ðžïļ ImÃĄgenes inline | VisualizaciÃģn de imÃĄgenes dentro del correo |
| ðïļ Carpetas | Bandeja de entrada y enviados sincronizados |
| ðĩ Indicadores | Correos leÃdos/no leÃdos con marcadores visuales |
ðïļ Arquitectura
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ARQUITECTURA DEL SISTEMA â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ââââââââââââââââââââââââââââ
â SERVIDOR TCP â
â servidor.py â
â ââââââââââââââââââââ â
â â GameServer â â
â â - Estado juego â â
â â - Broadcast â â
â â - Turnos â â
â ââââââââââââââââââââ â
â Puerto 3333 â
ââââââââââââââŽââââââââââââââ
â
ââââââââââââââīââââââââââââââââ
â Protocolo JSON â
â sobre TCP/IP â
ââââââââââââââŽââââââââââââââââ
âââââââââââââââââââââââžââââââââââââââââââââââ
â â â
âž âž âž
ââââââââââââââââââââ ââââââââââââââââââââ ââââââââââââââââââââ
â CLIENTE 1 â â CLIENTE 2 â â CLIENTE N â
â â â â â â
â ââââââââââââââ â â ââââââââââââââ â â ââââââââââââââ â
â â app.py â â â â app.py â â â â cliente_ â â
â â Dashboard â â â â Dashboard â â â â juego.py â â
â â + Juego â â â â + Juego â â â â Standalone â â
â ââââââââââââââ â â ââââââââââââââ â â ââââââââââââââ â
ââââââââââââââââââââ ââââââââââââââââââââ ââââââââââââââââââââ
ð Estructura del Proyecto
Proyecto1AVApsp/
â
âââ ð servidor.py # Servidor TCP del juego (371 lÃneas)
â âââ GameServer # Gestiona estado, turnos y broadcasts
â
âââ ð app.py # Dashboard principal (2566 lÃneas)
â âââ DashboardApp # AplicaciÃģn Tkinter completa
â âââ GameClient # Cliente TCP integrado
â
âââ ð cliente_juego.py # Cliente standalone (220 lÃneas)
â âââ GameClient # VersiÃģn ligera para jugar
â
âââ ð requirements.txt # Dependencias Python
âââ ð README.md # Este archivo
ð InstalaciÃģn
Requisitos Previos
- Python 3.8+
- pip (gestor de paquetes)
Paso 1: Clonar el Repositorio
git clone https://github.com/MarcosFerrandiz/Proyecto1AVApsp.git
cd Proyecto1AVApsp
Paso 2: Crear Entorno Virtual (Recomendado)
python -m venv .venv
# Linux/macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
Paso 3: Instalar Dependencias
pip install -r requirements.txt
ðĶ Ver dependencias detalladas
| Paquete | VersiÃģn | Uso |
|---|---|---|
psutil |
âĨ5.9.0 | Monitor de recursos del sistema |
matplotlib |
âĨ3.5.0 | GrÃĄficos en tiempo real |
pillow |
âĨ9.0.0 | Procesamiento de imÃĄgenes |
pygame |
âĨ2.1.0 | ReproducciÃģn de audio (opcional) |
requests |
âĨ2.32.0 | Peticiones HTTP (API, Scraping) |
beautifulsoup4 |
âĨ4.12.0 | Parsing HTML (Scraping) |
ðŊ Uso
ðĨïļ Iniciar el Servidor
python servidor.py
El servidor escucharÃĄ en
0.0.0.0:3333
ðŪ OpciÃģn A: Dashboard Completo
python app.py
Incluye el juego integrado + todas las funcionalidades del panel.
ðŪ OpciÃģn B: Cliente Standalone
python cliente_juego.py
Cliente ligero solo para jugar al Minesweeper.
ð Conectar al Juego
- Introduce el Host del servidor (por defecto:
127.0.0.1) - Verifica el Puerto (por defecto:
3333) - Pulsa "Conectar"
- ÂĄEspera a otro jugador y pulsa "Iniciar Juego"!
ðĢ MecÃĄnicas del Juego
ð Flujo de una Partida
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â FLUJO DEL JUEGO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â âââââââââââ âââââââââââââââ âââââââââââââââ â
â â LOBBY ââââââķâ COLOCACIÃN ââââââķâ BÃSQUEDA â â
â â â â DE BOMBAS â â (PLAYING) â â
â âââââââââââ âââââââââââââââ ââââââââŽâââââââ â
â â â â
â â âââââââââââââââââââââââââââââ â
â â â â
â â âž â
â â âââââââââââ ââââââââââââ ââââââââââââââ â
â â â ÂŋBOMBA? âââââķâ EXPLOSIÃNâââââķâ ÂŋVIDAS=0? â â
â â ââââââŽâââââ ââââââââââââ âââââââŽâââââââ â
â â â NO â â
â â âž SÃ âž â
â â âââââââââââ ââââââââââââ â
â â â SAFE â â GAME OVERâ â
â â â(casilla â ââââââââââââ â
â â â segura) â â
â â ââââââŽâââââ â
â â â â
â â âž â
â â âŋâââââââââââââââââââââââââââââââââââââ â
â â â SIGUIENTE TURNO â â
â â ââââââââââââââââââââââââââââââââââââââ â
â â â
â âââââââââââââââââââââķ (Nueva Partida) â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ð ProgresiÃģn de Dificultad
| Ronda | TamaÃąo Grid | Bombas/Jugador | Total Bombas* |
|---|---|---|---|
| 1ïļâĢ | 3Ã3 | 3 | 6 |
| 2ïļâĢ | 5Ã5 | 5 | 10 |
| 3ïļâĢ | 8Ã8 | 9 | 18 |
| 4ïļâĢ | 11Ã11 | 12 | 24 |
| 5ïļâĢ+ | 14Ã14 | 15 | 30 |
*Para 2 jugadores
ðŊ Fases del Juego
1. ðĢ Fase de ColocaciÃģn (PLACING)
- Cada jugador coloca bombas por turnos
- Las bombas se muestran brevemente (1 segundo) a todos
- ÂĄMemoriza dÃģnde pones TUS bombas y las del rival!
2. ð Fase de BÚsqueda (PLAYING)
- Excava casillas por turnos
- Casilla segura â Se marca en verde â
- Bomba â ÂĄEXPLOSIÃN! Pierdes 1 vida ð
ð Condiciones de Victoria
| CondiciÃģn | Resultado |
|---|---|
| Rival pierde todas las vidas | ÂĄGANASTE! ð |
| Todas las casillas seguras reveladas | ÂĄZona despejada! |
| Superas la ronda 5 | ÂĄVictoria total! ð |
ðĄ Protocolo de ComunicaciÃģn
Mensajes JSON Cliente â Servidor
// Iniciar partida
{ "type": "START_GAME" }
// Colocar bomba (fase PLACING)
{ "type": "PLACE_BOMB", "x": 2, "y": 1 }
// Excavar casilla (fase PLAYING)
{ "type": "CLICK_CELL", "x": 3, "y": 4 }
// Verificar zona despejada
{ "type": "CHECK_DUNGEON_CLEARED" }
Mensajes JSON Servidor â Clientes
// Nueva ronda
{
"type": "NEW_ROUND",
"round": 1,
"grid_size": 3,
"total_bombs_per_player": 3
}
// NotificaciÃģn de turno
{
"type": "TURN_NOTIFY",
"active_player": "('127.0.0.1', 54321)",
"msg": "Turno de ... para poner bombas."
}
// Flash de bomba (visible 1 segundo)
{
"type": "BOMB_flash",
"x": 1, "y": 2,
"who": "('127.0.0.1', 54321)"
}
// ExplosiÃģn
{
"type": "EXPLOSION",
"x": 1, "y": 2,
"who": "...",
"lives": 2
}
// Casilla segura
{ "type": "SAFE", "x": 0, "y": 0 }
// Game Over
{
"type": "GAME_OVER",
"loser": "...",
"msg": "ð ÂĄ... ha perdido todas sus vidas!"
}
ð§ DocumentaciÃģn TÃĐcnica Detallada
ð Sistema de IdentificaciÃģn de Jugadores
El servidor identifica a cada jugador mediante su direcciÃģn de socket, que es una tupla Única (IP, Puerto):
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â IDENTIFICACIÃN ÃNICA DE JUGADORES â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââĪ
â â
â Cuando un cliente se conecta al servidor: â
â â
â Cliente 1 âââââââš sock.accept() âââââââš ('127.0.0.1', 49956) â
â Cliente 2 âââââââš sock.accept() âââââââš ('127.0.0.1', 49968) â
â â
â Aunque ambos clientes estÃĐn en el MISMO ordenador (127.0.0.1) â
â el PUERTO es diferente y Único para cada conexiÃģn. â
â â
â Esta tupla (IP, Puerto) actÚa como IDENTIFICADOR ÃNICO. â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ÂŋCÃģmo funciona en el mismo ordenador?
# servidor.py - 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 3 Lenguaje base |
Tkinter Interfaz grÃĄfica |
TCP Sockets ComunicaciÃģn en red |
|
Matplotlib GrÃĄficos |
JSON Protocolo mensajes |
Requests APIs HTTP |
ð§ĩ Conceptos de PSP Aplicados
| Concepto | ImplementaciÃģn |
|---|---|
| Procesos | Lanzamiento de aplicaciones externas (VS Code, Firefox) |
| Threads | Servidor multihilo, cliente con hilos de recepciÃģn |
| Sockets TCP | ComunicaciÃģn cliente-servidor en red (Juego y Correo) |
| Servicios | API OpenWeather, scraping de Wallapop, IMAP/SMTP |
| SincronizaciÃģn | Locks para acceso concurrente a estado compartido |
ð§ CLIENTE DE CORREO ELECTRÃNICO: ARQUITECTURA Y FUNCIONAMIENTO
ðïļ Arquitectura del Sistema de Correo
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ARQUITECTURA DEL CLIENTE DE CORREO â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
âââââââââââââââââââââââââââââ
â SERVIDOR WEBMIN â
â 10.10.0.101 â
âââââââââââââââŽââââââââââââââ
â
âââââââââââââââââââââââīââââââââââââââââââââââ
â â
âââââââââžâââââââââ ââââââââââžâââââââââ
â PUERTO 143 â â PUERTO 25 â
â IMAP â â SMTP â
â (Lectura) â â (EnvÃo) â
âââââââââŽâââââââââ ââââââââââŽâââââââââ
â â
âââââââââââââââââââŽââââââââââââââââââââââââ
â
âââââââââââžâââââââââââ
â app.py â
â DashboardApp â
â â
â Tab "Correos" â
ââââââââââââââââââââââ
â
âââââââââââââââââââââââžââââââââââââââââââââââ
â â â
âââââââââžâââââââââ âââââââââââžâââââââââ ââââââââââžâââââââââ
â _connect_ â â _refresh_ â â _send_mail_ â
â mail_server() â â mail_list() â â with_attach() â
â â â â â â
â IMAP Login â â IMAP FETCH â â SMTP Send â
ââââââââââââââââââ ââââââââââââââââââââ âââââââââââââââââââ
ðĄ Protocolos Utilizados
IMAP (Internet Message Access Protocol)
Protocolo para leer correos del servidor. Puerto: 143 (sin TLS).
# 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Ãģnselect(mailbox)â Seleccionar carpeta (INBOX, Sent, etc.)search(None, 'ALL')â Buscar todos los correosfetch(id, '(BODY.PEEK[] FLAGS)')â Obtener correo sin marcarlo como leÃdostore(id, '+FLAGS', '\\Seen')â Marcar como leÃdoappend(folder, flags, date, msg)â Guardar correo en carpeta
SMTP (Simple Mail Transfer Protocol)
Protocolo para enviar correos. Puerto: 25 (sin TLS).
# 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
- Fork del proyecto
- Crea tu Feature Branch (
git checkout -b feature/NuevaFuncion) - Commit tus cambios (
git commit -m 'Add: Nueva funciÃģn') - Push a la rama (
git push origin feature/NuevaFuncion) - Abre un Pull Request
ð Licencia
Este proyecto es de carÃĄcter educativo y fue desarrollado como parte del mÃģdulo de ProgramaciÃģn de Servicios y Procesos.
Desarrollado con âĪïļ por Marcos Ferrandiz
Proyecto 1š EvaluaciÃģn - PSP (ProgramaciÃģn de Servicios y Procesos)