correo
This commit is contained in:
parent
f0dc49b62e
commit
f1906ec18e
|
|
@ -0,0 +1,44 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Configuración de correo (contiene credenciales)
|
||||
.mail_config.json
|
||||
|
||||
# Backups
|
||||
*.backup*
|
||||
*.bak
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
# Arquitectura del Cliente de Correo - Proyecto1AVApsp
|
||||
|
||||
## Estructura de la aplicación
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Proyecto1AVApsp │
|
||||
│ Panel de Laboratorio │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
||||
│ Tareas │ │ Correos │ │ Juegos │
|
||||
└─────────┘ └────┬────┘ └─────────┘
|
||||
│
|
||||
┌─────────────────┴─────────────────┐
|
||||
│ │
|
||||
┌────▼────────┐ ┌───────▼───────┐
|
||||
│ IMAP │ │ SMTP │
|
||||
│ Cliente │ │ Cliente │
|
||||
└─────┬───────┘ └───────┬───────┘
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
└─────────► Servidor ◄─────────┘
|
||||
│ Webmin │
|
||||
│ 10.10.0.101 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Componentes principales
|
||||
|
||||
### 1. Interfaz de Usuario (app.py:631-753)
|
||||
- Panel de configuración del servidor
|
||||
- Lista de correos (bandeja de entrada)
|
||||
- Visor de correos
|
||||
- Ventana de composición de correos
|
||||
|
||||
### 2. Gestión IMAP (app.py:1380-1540)
|
||||
**Funciones:**
|
||||
- `_connect_mail_server()`: Conecta al servidor IMAP
|
||||
- `_disconnect_mail_server()`: Desconecta del servidor
|
||||
- `_refresh_mail_list()`: Carga la lista de correos
|
||||
- `_on_mail_select()`: Maneja la selección de correos
|
||||
- `_display_mail()`: Muestra el contenido de un correo
|
||||
|
||||
**Protocolo:** IMAP (Puerto 143)
|
||||
|
||||
### 3. Gestión SMTP (app.py:1585-1620)
|
||||
**Funciones:**
|
||||
- `_open_compose_window()`: Abre ventana de redacción
|
||||
- `_send_mail()`: Envía correos usando SMTP
|
||||
|
||||
**Protocolo:** SMTP (Puerto 25)
|
||||
|
||||
## Flujo de datos
|
||||
|
||||
### Lectura de correos (IMAP)
|
||||
```
|
||||
Usuario → Clic "Conectar"
|
||||
↓
|
||||
_connect_mail_server()
|
||||
↓
|
||||
imaplib.IMAP4(host, 143)
|
||||
↓
|
||||
imap.login(usuario, contraseña)
|
||||
↓
|
||||
imap.select('INBOX')
|
||||
↓
|
||||
_refresh_mail_list()
|
||||
↓
|
||||
imap.search(None, 'ALL')
|
||||
↓
|
||||
imap.fetch(mail_id, 'RFC822')
|
||||
↓
|
||||
email.message_from_bytes()
|
||||
↓
|
||||
Mostrar en Listbox
|
||||
↓
|
||||
Usuario selecciona correo
|
||||
↓
|
||||
_display_mail()
|
||||
↓
|
||||
Parsear contenido (texto/HTML)
|
||||
↓
|
||||
Mostrar en ScrolledText
|
||||
```
|
||||
|
||||
### Envío de correos (SMTP)
|
||||
```
|
||||
Usuario → Clic "Nuevo correo"
|
||||
↓
|
||||
_open_compose_window()
|
||||
↓
|
||||
Usuario completa campos (Para, Asunto, Mensaje)
|
||||
↓
|
||||
Usuario → Clic "Enviar"
|
||||
↓
|
||||
_send_mail()
|
||||
↓
|
||||
MIMEMultipart()
|
||||
↓
|
||||
MIMEText(body, 'plain')
|
||||
↓
|
||||
smtplib.SMTP(host, 25)
|
||||
↓
|
||||
server.send_message(msg)
|
||||
↓
|
||||
Confirmación al usuario
|
||||
```
|
||||
|
||||
## Protocolos de comunicación
|
||||
|
||||
### IMAP (Internet Message Access Protocol)
|
||||
- **Puerto**: 143 (sin cifrar) / 993 (cifrado)
|
||||
- **Uso**: Leer correos del servidor
|
||||
- **Ventajas**:
|
||||
- Los correos permanecen en el servidor
|
||||
- Acceso desde múltiples dispositivos
|
||||
- Sincronización de carpetas
|
||||
|
||||
### SMTP (Simple Mail Transfer Protocol)
|
||||
- **Puerto**: 25 (sin cifrar) / 587 (TLS) / 465 (SSL)
|
||||
- **Uso**: Enviar correos
|
||||
- **Autenticación**: Opcional según configuración del servidor
|
||||
|
||||
## Configuración del servidor Webmin
|
||||
|
||||
### Servicios necesarios:
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Webmin (http://10.10.0.101:20000) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Servicios de correo: │
|
||||
│ • Postfix (SMTP) → Puerto 25 │
|
||||
│ • Dovecot (IMAP) → Puerto 143 │
|
||||
│ • Dovecot (POP3) → Puerto 110 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Pasos en Webmin:
|
||||
1. **Servers → Dovecot IMAP/POP3 Server**
|
||||
- Habilitar servicio IMAP en puerto 143
|
||||
- Configurar usuarios y contraseñas
|
||||
|
||||
2. **Servers → Postfix Mail Server**
|
||||
- Habilitar servicio SMTP en puerto 25
|
||||
- Configurar dominio y relay
|
||||
|
||||
3. **System → Users and Groups**
|
||||
- Crear usuarios del sistema para correo
|
||||
- Asignar contraseñas
|
||||
|
||||
## Seguridad
|
||||
|
||||
### Advertencias actuales:
|
||||
⚠️ **La implementación actual usa conexiones sin cifrar**
|
||||
|
||||
### Recomendaciones:
|
||||
1. Usar IMAPS (puerto 993) en lugar de IMAP (143)
|
||||
2. Usar SMTPS (puerto 465/587) en lugar de SMTP (25)
|
||||
3. Implementar SSL/TLS en las conexiones
|
||||
4. No usar contraseñas en texto plano en el código
|
||||
5. Usar autenticación del servidor SMTP
|
||||
|
||||
### Mejora de seguridad (código):
|
||||
```python
|
||||
# IMAP con SSL
|
||||
import imaplib
|
||||
imap = imaplib.IMAP4_SSL('10.10.0.101', 993)
|
||||
|
||||
# SMTP con TLS
|
||||
import smtplib
|
||||
smtp = smtplib.SMTP('10.10.0.101', 587)
|
||||
smtp.starttls()
|
||||
smtp.login(username, password)
|
||||
```
|
||||
|
||||
## Estructura de archivos
|
||||
|
||||
```
|
||||
Proyecto1AVApsp/
|
||||
├── app.py # Aplicación principal con cliente de correo
|
||||
├── CORREO_README.md # Documentación de usuario
|
||||
├── test_mail_server.py # Script de prueba de conectividad
|
||||
├── requirements.txt # Dependencias (smtplib/imaplib incluidos en Python)
|
||||
└── README.md # Documentación general del proyecto
|
||||
```
|
||||
|
||||
## Variables de estado
|
||||
|
||||
```python
|
||||
# Variables del cliente de correo (en DashboardApp)
|
||||
self.mail_connected = False # Estado de conexión
|
||||
self.imap_connection = None # Objeto de conexión IMAP
|
||||
self.current_mailbox = 'INBOX' # Bandeja actual
|
||||
self.mail_list = [] # Lista de correos cargados
|
||||
|
||||
# Widgets de UI
|
||||
self.mail_imap_host # Entry: servidor IMAP
|
||||
self.mail_imap_port # Entry: puerto IMAP
|
||||
self.mail_smtp_host # Entry: servidor SMTP
|
||||
self.mail_smtp_port # Entry: puerto SMTP
|
||||
self.mail_username # Entry: usuario
|
||||
self.mail_password # Entry: contraseña
|
||||
self.mail_listbox # Listbox: lista de correos
|
||||
self.mail_body_text # ScrolledText: cuerpo del correo
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Script de prueba de conectividad:
|
||||
```bash
|
||||
python3 test_mail_server.py
|
||||
```
|
||||
|
||||
Este script verifica:
|
||||
- Conexión a Webmin (puerto 20000)
|
||||
- Disponibilidad de SMTP (puerto 25)
|
||||
- Disponibilidad de IMAP (puerto 143)
|
||||
- Disponibilidad de POP3 (puerto 110)
|
||||
|
||||
## Próximas mejoras
|
||||
|
||||
1. **Seguridad**
|
||||
- [ ] Implementar SSL/TLS
|
||||
- [ ] Autenticación segura
|
||||
- [ ] Gestión de certificados
|
||||
|
||||
2. **Funcionalidad**
|
||||
- [ ] Soporte para adjuntos
|
||||
- [ ] Vista HTML mejorada
|
||||
- [ ] Múltiples carpetas
|
||||
- [ ] Búsqueda de correos
|
||||
- [ ] Responder/Reenviar
|
||||
|
||||
3. **UI/UX**
|
||||
- [ ] Indicador de correos no leídos
|
||||
- [ ] Filtros y ordenamiento
|
||||
- [ ] Marcadores/etiquetas
|
||||
- [ ] Vista previa de adjuntos
|
||||
|
||||
4. **Performance**
|
||||
- [ ] Carga asíncrona de correos
|
||||
- [ ] Cache de correos
|
||||
- [ ] Paginación
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
# Cliente de Correo Electrónico - Proyecto1AVApsp
|
||||
|
||||
## Descripción
|
||||
|
||||
Se ha implementado un cliente de correo electrónico completo en la pestaña "Correos" de la aplicación. Este cliente permite conectarse a tu servidor Webmin para leer y enviar correos electrónicos.
|
||||
|
||||
## Características
|
||||
|
||||
- **Conexión IMAP**: Lee correos desde tu servidor (Puerto 143)
|
||||
- **Conexión SMTP**: Envía correos nuevos (Puerto 25)
|
||||
- **Interfaz tipo Outlook**: Panel dividido con lista de correos y visor
|
||||
- **Redacción de correos**: Ventana dedicada para componer nuevos mensajes
|
||||
|
||||
## Configuración del Servidor Webmin
|
||||
|
||||
### Datos de conexión predeterminados:
|
||||
|
||||
- **Servidor IMAP**: `10.10.0.101`
|
||||
- **Puerto IMAP**: `143`
|
||||
- **Servidor SMTP**: `10.10.0.101`
|
||||
- **Puerto SMTP**: `25`
|
||||
|
||||
### Puertos configurados en Webmin:
|
||||
|
||||
- SMTP (envío de correo): 25
|
||||
- IMAP (consulta de correo): 143
|
||||
- POP (descarga de correo): 110 (no usado en esta implementación)
|
||||
- Interfaz web Webmin: http://10.10.0.101:20000
|
||||
|
||||
## Cómo usar el cliente de correo
|
||||
|
||||
### 1. Ejecutar la aplicación
|
||||
|
||||
```bash
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### 2. Ir a la pestaña "Correos"
|
||||
|
||||
La pestaña está ubicada en el panel central de la aplicación.
|
||||
|
||||
### 3. Configurar la conexión
|
||||
|
||||
En el panel "Configuración del servidor":
|
||||
|
||||
- **Servidor IMAP**: Ya está configurado como `10.10.0.101`
|
||||
- **Puerto IMAP**: Ya está configurado como `143`
|
||||
- **Servidor SMTP**: Ya está configurado como `10.10.0.101`
|
||||
- **Puerto SMTP**: Ya está configurado como `25`
|
||||
- **Usuario**: Introduce tu nombre de usuario del servidor de correo
|
||||
- **Contraseña**: Introduce tu contraseña
|
||||
|
||||
### 4. Conectar al servidor
|
||||
|
||||
Haz clic en el botón **"Conectar"**. Si la conexión es exitosa:
|
||||
- El estado cambiará a "Conectado" en verde
|
||||
- Se cargarán automáticamente los correos de la bandeja de entrada
|
||||
|
||||
### 5. Leer correos
|
||||
|
||||
- Los correos aparecen en la lista de la izquierda
|
||||
- Haz clic en un correo para ver su contenido completo
|
||||
- Se muestran:
|
||||
- Remitente (De)
|
||||
- Asunto
|
||||
- Fecha
|
||||
- Cuerpo del mensaje
|
||||
|
||||
### 6. Actualizar la lista de correos
|
||||
|
||||
Haz clic en el botón **"Actualizar"** para recargar la lista de correos desde el servidor.
|
||||
|
||||
### 7. Enviar un nuevo correo
|
||||
|
||||
1. Haz clic en el botón **"Nuevo correo"**
|
||||
2. Se abrirá una ventana de composición
|
||||
3. Completa los campos:
|
||||
- **Para**: Dirección del destinatario
|
||||
- **Asunto**: Título del correo
|
||||
- **Mensaje**: Contenido del correo
|
||||
4. Haz clic en **"Enviar"**
|
||||
|
||||
### 8. Desconectar
|
||||
|
||||
Cuando termines, haz clic en **"Desconectar"** para cerrar la conexión con el servidor.
|
||||
|
||||
## Notas técnicas
|
||||
|
||||
### Bibliotecas utilizadas
|
||||
|
||||
El cliente utiliza las bibliotecas estándar de Python:
|
||||
- `imaplib`: Para leer correos (protocolo IMAP)
|
||||
- `smtplib`: Para enviar correos (protocolo SMTP)
|
||||
- `email`: Para parsear y crear mensajes de correo
|
||||
|
||||
### Limitaciones actuales
|
||||
|
||||
1. **Seguridad**:
|
||||
- La conexión IMAP es sin cifrado (puerto 143 en lugar de 993 IMAPS)
|
||||
- La conexión SMTP es sin cifrado (puerto 25 en lugar de 465/587 SMTPS)
|
||||
- Para producción, se recomienda usar conexiones cifradas
|
||||
|
||||
2. **Funcionalidades**:
|
||||
- Solo se muestran los últimos 50 correos
|
||||
- No hay soporte para adjuntos aún
|
||||
- Solo se muestra la bandeja de entrada (INBOX)
|
||||
- Los correos HTML se muestran como texto crudo
|
||||
|
||||
3. **Autenticación SMTP**:
|
||||
- El código actual no utiliza autenticación para SMTP
|
||||
- Si tu servidor Webmin requiere autenticación, necesitarás descomentar estas líneas en el código:
|
||||
```python
|
||||
# server.starttls()
|
||||
# server.login(username, password)
|
||||
```
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### Error: "No se pudo conectar al servidor"
|
||||
|
||||
1. Verifica que el servidor Webmin esté en ejecución
|
||||
2. Comprueba que puedes hacer ping a `10.10.0.101`
|
||||
3. Verifica que los puertos 143 y 25 estén abiertos:
|
||||
```bash
|
||||
telnet 10.10.0.101 143
|
||||
telnet 10.10.0.101 25
|
||||
```
|
||||
|
||||
### Error: "Login failed"
|
||||
|
||||
- Verifica que el usuario y contraseña sean correctos
|
||||
- Comprueba que la cuenta de correo esté configurada en Webmin
|
||||
|
||||
### No se cargan los correos
|
||||
|
||||
- Haz clic en "Actualizar" para recargar
|
||||
- Verifica la configuración de la bandeja INBOX en el servidor
|
||||
- Revisa el panel de notas para ver mensajes de error
|
||||
|
||||
## Mejoras futuras
|
||||
|
||||
- [ ] Soporte para conexiones cifradas (IMAPS/SMTPS)
|
||||
- [ ] Gestión de adjuntos
|
||||
- [ ] Múltiples carpetas/bandejas
|
||||
- [ ] Búsqueda de correos
|
||||
- [ ] Marcado como leído/no leído
|
||||
- [ ] Eliminación de correos
|
||||
- [ ] Responder y reenviar correos
|
||||
- [ ] Vista HTML mejorada
|
||||
- [ ] Configuración persistente
|
||||
|
||||
## Contacto
|
||||
|
||||
Para más información sobre la configuración del servidor Webmin, consulta:
|
||||
- Interfaz web Webmin: http://10.10.0.101:20000
|
||||
718
README.md
718
README.md
|
|
@ -71,6 +71,18 @@
|
|||
- 🔗 **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
|
||||
|
|
@ -634,12 +646,714 @@ class GameServer:
|
|||
|----------|----------------|
|
||||
| **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 |
|
||||
| **Servicios** | API OpenWeather, scraping de Wallapop |
|
||||
| **Sockets TCP** | Comunicación cliente-servidor en red (Juego y Correo) |
|
||||
| **Servicios** | API OpenWeather, scraping de Wallapop, IMAP/SMTP |
|
||||
| **Sincronización** | Locks para acceso concurrente a estado compartido |
|
||||
|
||||
---
|
||||
|
||||
## 📧 CLIENTE DE CORREO ELECTRÓNICO: ARQUITECTURA Y FUNCIONAMIENTO
|
||||
|
||||
### 🏗️ Arquitectura del Sistema de Correo
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ARQUITECTURA DEL CLIENTE DE CORREO │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌───────────────────────────┐
|
||||
│ SERVIDOR WEBMIN │
|
||||
│ 10.10.0.101 │
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
┌─────────────────────┴─────────────────────┐
|
||||
│ │
|
||||
┌───────▼────────┐ ┌────────▼────────┐
|
||||
│ PUERTO 143 │ │ PUERTO 25 │
|
||||
│ IMAP │ │ SMTP │
|
||||
│ (Lectura) │ │ (Envío) │
|
||||
└───────┬────────┘ └────────┬────────┘
|
||||
│ │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│
|
||||
┌─────────▼──────────┐
|
||||
│ app.py │
|
||||
│ DashboardApp │
|
||||
│ │
|
||||
│ Tab "Correos" │
|
||||
└────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
┌───────▼────────┐ ┌─────────▼────────┐ ┌────────▼────────┐
|
||||
│ _connect_ │ │ _refresh_ │ │ _send_mail_ │
|
||||
│ mail_server() │ │ mail_list() │ │ with_attach() │
|
||||
│ │ │ │ │ │
|
||||
│ IMAP Login │ │ IMAP FETCH │ │ SMTP Send │
|
||||
└────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 📡 Protocolos Utilizados
|
||||
|
||||
#### **IMAP (Internet Message Access Protocol)**
|
||||
Protocolo para **leer correos** del servidor. Puerto: **143** (sin TLS).
|
||||
|
||||
```python
|
||||
# Conexión IMAP (app.py líneas 1578-1582)
|
||||
import imaplib
|
||||
self.imap_connection = imaplib.IMAP4(host, port_num) # Puerto 143
|
||||
self.imap_connection.login(username, password)
|
||||
```
|
||||
|
||||
**Comandos IMAP utilizados:**
|
||||
- `login(user, pass)` → Autenticación
|
||||
- `select(mailbox)` → Seleccionar carpeta (INBOX, Sent, etc.)
|
||||
- `search(None, 'ALL')` → Buscar todos los correos
|
||||
- `fetch(id, '(BODY.PEEK[] FLAGS)')` → Obtener correo sin marcarlo como leído
|
||||
- `store(id, '+FLAGS', '\\Seen')` → Marcar como leído
|
||||
- `append(folder, flags, date, msg)` → Guardar correo en carpeta
|
||||
|
||||
#### **SMTP (Simple Mail Transfer Protocol)**
|
||||
Protocolo para **enviar correos**. Puerto: **25** (sin TLS).
|
||||
|
||||
```python
|
||||
# Conexión SMTP (app.py líneas 2949-2955)
|
||||
import smtplib
|
||||
with smtplib.SMTP(smtp_host, smtp_port_num, timeout=15) as server:
|
||||
server.send_message(msg, to_addrs=recipients)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔐 Sistema de Credenciales
|
||||
|
||||
#### **Guardado Automático con Base64**
|
||||
|
||||
```python
|
||||
# GUARDAR (app.py líneas 1540-1557)
|
||||
import base64
|
||||
config = {
|
||||
'imap_host': '10.10.0.101',
|
||||
'imap_port': '143',
|
||||
'smtp_host': '10.10.0.101',
|
||||
'smtp_port': '25',
|
||||
'username': 'marcos@psp.es',
|
||||
'password': base64.b64encode(password.encode()).decode() # Codificar
|
||||
}
|
||||
json.dump(config, open('.mail_config.json', 'w'), indent=2)
|
||||
```
|
||||
|
||||
```python
|
||||
# CARGAR (app.py líneas 1512-1520)
|
||||
config = json.load(open('.mail_config.json', 'r'))
|
||||
password = base64.b64decode(config['password']).decode() # Decodificar
|
||||
```
|
||||
|
||||
**Flujo de guardado:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ FLUJO DE GUARDADO DE CREDENCIALES │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Usuario ingresa credenciales │
|
||||
│ └─► Usuario: marcos@psp.es │
|
||||
│ └─► Password: 1234 │
|
||||
│ │
|
||||
│ 2. Marca checkbox "💾 Recordar credenciales" │
|
||||
│ └─► mail_remember_var.get() = True │
|
||||
│ │
|
||||
│ 3. Al conectar exitosamente, se llama _save_mail_credentials() │
|
||||
│ │
|
||||
│ 4. Base64 encoding: │
|
||||
│ Password "1234" → Bytes b'1234' │
|
||||
│ → Base64 b'MTIzNA==' │
|
||||
│ → String "MTIzNA==" │
|
||||
│ │
|
||||
│ 5. Se guarda en .mail_config.json: │
|
||||
│ { │
|
||||
│ "username": "marcos@psp.es", │
|
||||
│ "password": "MTIzNA==" │
|
||||
│ } │
|
||||
│ │
|
||||
│ 6. Al reiniciar app.py: │
|
||||
│ └─► _load_mail_credentials() lee el archivo │
|
||||
│ └─► Decodifica Base64: "MTIzNA==" → "1234" │
|
||||
│ └─► Precarga los campos automáticamente │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANTE:** Base64 NO es encriptación, solo ofuscación. El archivo `.mail_config.json` está protegido en `.gitignore` para no subirlo a Git.
|
||||
|
||||
---
|
||||
|
||||
### 📬 Lectura de Correos (IMAP)
|
||||
|
||||
#### **Función Principal: `_refresh_mail_list()` (líneas 1635-1801)**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ FLUJO DE LECTURA DE CORREOS │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. SELECT 'INBOX' │
|
||||
│ └─► imap_connection.select('INBOX') │
|
||||
│ │
|
||||
│ 2. SEARCH ALL │
|
||||
│ └─► status, messages = imap_connection.search(None, 'ALL') │
|
||||
│ └─► mail_ids = messages[0].split() # [b'1', b'2', b'3', ...] │
|
||||
│ │
|
||||
│ 3. FETCH con BODY.PEEK[] (no marca como leído) │
|
||||
│ for mail_id in mail_ids: │
|
||||
│ status, msg_data = imap_connection.fetch( │
|
||||
│ mail_id, │
|
||||
│ '(BODY.PEEK[] FLAGS)' # ← PEEK es clave │
|
||||
│ ) │
|
||||
│ │
|
||||
│ 4. EXTRAER FLAGS (\Seen) │
|
||||
│ └─► Busca en respuesta IMAP: "FLAGS (\\Seen ...)" │
|
||||
│ └─► is_seen = True si contiene "\\Seen" │
|
||||
│ │
|
||||
│ 5. PARSEAR EMAIL │
|
||||
│ └─► msg = email.message_from_bytes(msg_data) │
|
||||
│ └─► from_addr = decode_header(msg['From'])[0][0] │
|
||||
│ └─► subject = decode_header(msg['Subject'])[0][0] │
|
||||
│ │
|
||||
│ 6. GUARDAR EN LISTA LOCAL │
|
||||
│ self.mail_list.append({ │
|
||||
│ 'id': mail_id, │
|
||||
│ 'from': from_addr, │
|
||||
│ 'subject': subject, │
|
||||
│ 'date': date_str, │
|
||||
│ 'msg': msg, # Objeto email completo │
|
||||
│ 'is_seen': is_seen │
|
||||
│ }) │
|
||||
│ │
|
||||
│ 7. MOSTRAR EN LISTBOX │
|
||||
│ if is_seen: │
|
||||
│ display = f' {from_addr} - {subject}' # Sin emoji │
|
||||
│ itemconfig(idx, fg='#888888') # Gris │
|
||||
│ else: │
|
||||
│ display = f'🔵 {from_addr} - {subject}' # Con emoji │
|
||||
│ itemconfig(idx, fg='#000000') # Negro │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### **¿Por qué BODY.PEEK[]?**
|
||||
|
||||
```python
|
||||
# ❌ MAL: RFC822 marca el correo como leído automáticamente
|
||||
fetch(mail_id, 'RFC822')
|
||||
|
||||
# ✅ BIEN: BODY.PEEK[] lee sin cambiar el flag \Seen
|
||||
fetch(mail_id, '(BODY.PEEK[] FLAGS)')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📤 Envío de Correos (SMTP)
|
||||
|
||||
#### **Función: `_send_mail_with_attachments()` (líneas 2837-2977)**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ FLUJO DE ENVÍO DE CORREO │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. VALIDAR DESTINATARIOS │
|
||||
│ to_addr_raw = "marcos@psp.es, user2@example.com" │
|
||||
│ └─► Split por comas/punto y coma │
|
||||
│ └─► recipients = ['marcos@psp.es', 'user2@example.com'] │
|
||||
│ └─► Validar regex: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,} │
|
||||
│ │
|
||||
│ 2. CREAR MENSAJE MIME MULTIPART │
|
||||
│ from email.mime.multipart import MIMEMultipart │
|
||||
│ msg = MIMEMultipart() │
|
||||
│ msg['From'] = 'marcos@psp.es' │
|
||||
│ msg['To'] = 'marcos@psp.es, user2@example.com' │
|
||||
│ msg['Subject'] = 'Asunto del correo' │
|
||||
│ │
|
||||
│ 3. ADJUNTAR CUERPO │
|
||||
│ from email.mime.text import MIMEText │
|
||||
│ msg.attach(MIMEText(body, 'plain', 'utf-8')) │
|
||||
│ │
|
||||
│ 4. ADJUNTAR ARCHIVOS │
|
||||
│ for file_path in attachments: │
|
||||
│ if file_ext == '.png': │
|
||||
│ part = MIMEImage(file_data, name=filename) │
|
||||
│ elif file_ext == '.pdf': │
|
||||
│ part = MIMEApplication(file_data, _subtype='pdf') │
|
||||
│ # ... otros tipos │
|
||||
│ msg.attach(part) │
|
||||
│ │
|
||||
│ 5. CONECTAR SMTP Y ENVIAR │
|
||||
│ with smtplib.SMTP('10.10.0.101', 25) as server: │
|
||||
│ server.send_message(msg, to_addrs=recipients) │
|
||||
│ │
|
||||
│ 6. GUARDAR EN CARPETA SENT (IMAP) │
|
||||
│ └─► _save_to_sent_folder(msg) # Ver siguiente sección │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### **Tipos MIME Soportados**
|
||||
|
||||
| Extensión | Tipo MIME | Clase Python |
|
||||
|-----------|-----------|--------------|
|
||||
| `.png`, `.jpg` | `image/*` | `MIMEImage` |
|
||||
| `.pdf` | `application/pdf` | `MIMEApplication` |
|
||||
| `.doc`, `.docx` | `application/msword` | `MIMEApplication` |
|
||||
| `.xls`, `.xlsx` | `application/vnd.ms-excel` | `MIMEApplication` |
|
||||
| `.zip`, `.rar` | `application/zip` | `MIMEApplication` |
|
||||
| `.txt` | `text/plain` | `MIMEText` |
|
||||
| Otros | `application/octet-stream` | `MIMEBase` |
|
||||
|
||||
---
|
||||
|
||||
### 💾 Guardado en Servidor IMAP
|
||||
|
||||
#### **Función: `_save_to_sent_folder()` (líneas 2979-3033)**
|
||||
|
||||
Después de enviar un correo por SMTP, se guarda una copia en la carpeta "Sent" del servidor IMAP para que aparezca en Webmin y otros clientes.
|
||||
|
||||
```python
|
||||
# Intentar múltiples nombres de carpeta
|
||||
sent_folders = ['Sent', 'INBOX.Sent', 'Enviados', 'INBOX.Enviados', 'Sent Items']
|
||||
|
||||
for folder in sent_folders:
|
||||
try:
|
||||
# Intentar seleccionar la carpeta
|
||||
status, _ = self.imap_connection.select(folder)
|
||||
if status == 'OK':
|
||||
# Guardar correo con APPEND
|
||||
self.imap_connection.append(
|
||||
folder,
|
||||
'\\Seen', # Marcar como leído
|
||||
imaplib.Time2Internaldate(time.time()),
|
||||
msg.as_bytes()
|
||||
)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
# Si no existe, crearla
|
||||
if not folder_found:
|
||||
self.imap_connection.create('Sent')
|
||||
self.imap_connection.append('Sent', '\\Seen', date, msg_bytes)
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ GUARDADO EN CARPETA SENT DEL SERVIDOR │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Usuario envía correo con SMTP │
|
||||
│ └─► Correo enviado a marcos@psp.es │
|
||||
│ │
|
||||
│ 2. _save_to_sent_folder() se ejecuta automáticamente │
|
||||
│ │
|
||||
│ 3. Intenta conectar a carpeta "Sent": │
|
||||
│ ├─► Intenta: SELECT 'Sent' ❌ Falla │
|
||||
│ ├─► Intenta: SELECT 'INBOX.Sent' ✅ OK │
|
||||
│ └─► Carpeta encontrada │
|
||||
│ │
|
||||
│ 4. Guarda el correo con APPEND: │
|
||||
│ APPEND "INBOX.Sent" (\Seen) "19-Feb-2026 19:30:00" {bytes} │
|
||||
│ │
|
||||
│ 5. Resultado: │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ SERVIDOR WEBMIN │ │
|
||||
│ │ ├─ INBOX (3 correos) │ │
|
||||
│ │ └─ Sent (1 correo NUEVO) ← AQUÍ │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 6. Al abrir Webmin: │
|
||||
│ └─► El correo aparece en "Sent" │
|
||||
│ └─► Otros clientes (Thunderbird, etc.) también lo ven │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 👥 Envío a Múltiples Destinatarios
|
||||
|
||||
#### **Validación y Parsing (líneas 2703-2740)**
|
||||
|
||||
```python
|
||||
# Entrada del usuario
|
||||
to_addr_raw = "marcos@psp.es, user2@example.com; user3@test.org"
|
||||
|
||||
# 1. Split por comas O punto y coma
|
||||
import re
|
||||
recipients = re.split(r'[;,]\s*', to_addr_raw)
|
||||
# → ['marcos@psp.es', 'user2@example.com', 'user3@test.org']
|
||||
|
||||
# 2. Limpiar espacios
|
||||
recipients = [r.strip() for r in recipients if r.strip()]
|
||||
|
||||
# 3. Validar formato con regex
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
invalid_emails = [email for email in recipients if not re.match(email_pattern, email)]
|
||||
|
||||
if invalid_emails:
|
||||
messagebox.showwarning('⚠️ Advertencia',
|
||||
f'Los siguientes emails no son válidos:\n{", ".join(invalid_emails)}')
|
||||
return
|
||||
|
||||
# 4. Confirmar si son múltiples
|
||||
if len(recipients) > 1:
|
||||
confirm = messagebox.askyesno('📧 Múltiples destinatarios',
|
||||
f'¿Enviar correo a {len(recipients)} destinatarios?\n\n' +
|
||||
'\n'.join(f' • {email}' for email in recipients))
|
||||
if not confirm:
|
||||
return
|
||||
|
||||
# 5. Enviar a todos
|
||||
self._send_mail_with_attachments(recipients, subject, body, attachments)
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ENVÍO A MÚLTIPLES DESTINATARIOS │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Usuario escribe en campo "Para:": │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ marcos@psp.es, user2@test.com, user3@example.org │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Al hacer clic en "📤 ENVIAR": │
|
||||
│ │
|
||||
│ 1. Parse: Split por ',' o ';' │
|
||||
│ ['marcos@psp.es', 'user2@test.com', 'user3@example.org'] │
|
||||
│ │
|
||||
│ 2. Validación regex de cada email │
|
||||
│ ✅ marcos@psp.es → Válido │
|
||||
│ ✅ user2@test.com → Válido │
|
||||
│ ✅ user3@example.org → Válido │
|
||||
│ │
|
||||
│ 3. Diálogo de confirmación: │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ ¿Enviar a 3 destinatarios? │ │
|
||||
│ │ │ │
|
||||
│ │ • marcos@psp.es │ │
|
||||
│ │ • user2@test.com │ │
|
||||
│ │ • user3@example.org │ │
|
||||
│ │ │ │
|
||||
│ │ [Sí] [No] │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 4. SMTP envía a todos: │
|
||||
│ msg['To'] = 'marcos@psp.es, user2@test.com, user3@example.org' │
|
||||
│ server.send_message(msg, to_addrs=[...]) │
|
||||
│ │
|
||||
│ 5. Mensaje de éxito: │
|
||||
│ ✅ "Correo enviado correctamente a 3 destinatarios" │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📎 Manejo de Adjuntos
|
||||
|
||||
#### **Adjuntar Archivos**
|
||||
|
||||
```python
|
||||
# Usuario hace clic en "📎 Adjuntar archivo"
|
||||
file_paths = filedialog.askopenfilenames(
|
||||
title='Seleccionar archivos',
|
||||
filetypes=[
|
||||
('Imágenes', '*.png *.jpg *.jpeg *.gif'),
|
||||
('PDFs', '*.pdf'),
|
||||
('Documentos', '*.doc *.docx *.xls *.xlsx'),
|
||||
('Todos', '*.*')
|
||||
]
|
||||
)
|
||||
|
||||
# Se guardan en lista
|
||||
attachments.append(file_path)
|
||||
|
||||
# Al enviar, se procesan:
|
||||
for file_path in attachments:
|
||||
file_name = os.path.basename(file_path) # "documento.pdf"
|
||||
file_ext = os.path.splitext(file_path)[1] # ".pdf"
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
file_data = f.read()
|
||||
|
||||
if file_ext == '.pdf':
|
||||
part = MIMEApplication(file_data, _subtype='pdf')
|
||||
part.add_header('Content-Disposition', 'attachment', filename=file_name)
|
||||
msg.attach(part)
|
||||
```
|
||||
|
||||
#### **Imágenes Inline con Ctrl+V**
|
||||
|
||||
```python
|
||||
# Usuario copia una imagen y presiona Ctrl+V
|
||||
def on_paste(event):
|
||||
try:
|
||||
# Obtener imagen del portapapeles
|
||||
img = ImageGrab.grabclipboard()
|
||||
|
||||
if img:
|
||||
# Guardar temporalmente
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
|
||||
img.save(temp_file.name)
|
||||
|
||||
# Mostrar miniatura en interfaz
|
||||
thumbnail = img.resize((150, 150))
|
||||
photo = ImageTk.PhotoImage(thumbnail)
|
||||
label = tk.Label(frame, image=photo)
|
||||
label.image = photo # Mantener referencia
|
||||
label.pack()
|
||||
|
||||
# Agregar a lista de adjuntos
|
||||
inline_images_data.append({'data': img_bytes})
|
||||
except:
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🖼️ Visualización de Correos
|
||||
|
||||
#### **Función: `_display_mail()` (líneas 2055-2337)**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ VISUALIZACIÓN DE CORREO │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Usuario hace clic en un correo de la lista │
|
||||
│ └─► _on_mail_select() → _display_mail(mail_info) │
|
||||
│ │
|
||||
│ 2. Marcar como leído en servidor (si es INBOX y no leído) │
|
||||
│ if not mail_info['is_seen']: │
|
||||
│ imap_connection.store(mail_id, '+FLAGS', '\\Seen') │
|
||||
│ # Actualizar visualmente: quitar 🔵, poner gris │
|
||||
│ │
|
||||
│ 3. Actualizar encabezados │
|
||||
│ mail_from_label.config(text='De: marcos@psp.es') │
|
||||
│ mail_subject_label.config(text='Asunto: Test') │
|
||||
│ mail_date_label.config(text='Fecha: 19/02/2026') │
|
||||
│ │
|
||||
│ 4. Procesar contenido multipart │
|
||||
│ if msg.is_multipart(): │
|
||||
│ for part in msg.walk(): │
|
||||
│ if content_type == 'text/plain': │
|
||||
│ body = part.get_payload(decode=True).decode() │
|
||||
│ elif part.get_filename(): # Adjunto │
|
||||
│ attachments.append({...}) │
|
||||
│ │
|
||||
│ 5. Mostrar cuerpo en Text widget │
|
||||
│ mail_body_text.delete('1.0', 'end') │
|
||||
│ mail_body_text.insert('1.0', body) │
|
||||
│ │
|
||||
│ 6. Mostrar imágenes inline (si PIL está disponible) │
|
||||
│ for att in images: │
|
||||
│ img = Image.open(BytesIO(att['data'])) │
|
||||
│ img.thumbnail((500, 500)) # Redimensionar │
|
||||
│ photo = ImageTk.PhotoImage(img) │
|
||||
│ mail_body_text.image_create('end', image=photo) │
|
||||
│ │
|
||||
│ 7. Mostrar otros adjuntos (PDFs, docs, etc.) │
|
||||
│ for att in other_attachments: │
|
||||
│ # Frame con icono, nombre, tamaño y botón "💾 Guardar" │
|
||||
│ icon = '📄' if PDF else '📝' if Word else '📎' │
|
||||
│ Button(text='💾 Guardar', command=save_file) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔵 Sistema de Indicadores Visuales
|
||||
|
||||
```python
|
||||
# Al cargar correos (líneas 1775-1790)
|
||||
for mail in mail_list:
|
||||
if is_seen:
|
||||
# Correo leído
|
||||
display_text = f' {from_addr[:27]} - {subject[:37]}'
|
||||
self.mail_listbox.insert('end', display_text)
|
||||
idx = self.mail_listbox.size() - 1
|
||||
self.mail_listbox.itemconfig(idx,
|
||||
fg='#888888', # Gris
|
||||
selectforeground='#666666'
|
||||
)
|
||||
else:
|
||||
# Correo NO leído
|
||||
display_text = f'🔵 {from_addr[:27]} - {subject[:37]}'
|
||||
self.mail_listbox.insert('end', display_text)
|
||||
idx = self.mail_listbox.size() - 1
|
||||
self.mail_listbox.itemconfig(idx,
|
||||
fg='#000000', # Negro
|
||||
selectforeground='#1a73e8'
|
||||
)
|
||||
|
||||
# Contador de no leídos
|
||||
self.unread_count = sum(1 for m in self.mail_list if not m.get('is_seen', False))
|
||||
self.unread_label.config(text=f'Correos sin leer: {self.unread_count}')
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ APARIENCIA VISUAL EN LISTBOX │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📬 Bandeja de entrada Correos sin leer: 2 │ │
|
||||
│ ├───────────────────────────────────────────────────────────────┤ │
|
||||
│ │ │ │
|
||||
│ │ 🔵 marcos@psp.es - Test para grabación ← NO LEÍDO (negro) │ │
|
||||
│ │ 🔵 user@example.com - Propuesta proyecto ← NO LEÍDO (negro) │ │
|
||||
│ │ admin@server.com - Notificación ← LEÍDO (gris) │ │
|
||||
│ │ webmaster@test.org - Informe ← LEÍDO (gris) │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Al hacer clic en el primero (🔵 NO LEÍDO): │
|
||||
│ 1. Se marca como leído en el servidor (STORE +FLAGS \Seen) │
|
||||
│ 2. Se actualiza la visualización: │
|
||||
│ ├─ Quita el emoji 🔵 │
|
||||
│ ├─ Cambia color a gris │
|
||||
│ └─ Decrementa contador: "Correos sin leer: 1" │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🛡️ Manejo de Errores y Log
|
||||
|
||||
```python
|
||||
# Función de log (líneas 4304-4315)
|
||||
def _log(self, text: str) -> None:
|
||||
# Verificar si estamos en hilo principal
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
self.after(0, lambda t=text: self._log(t))
|
||||
return
|
||||
|
||||
# Verificar si el widget notes existe
|
||||
if not hasattr(self, 'notes') or self.notes is None:
|
||||
print(f'[LOG] {text}') # Consola durante inicialización
|
||||
return
|
||||
|
||||
# Log normal en interfaz
|
||||
timestamp = datetime.datetime.now().strftime('%H:%M:%S')
|
||||
self.notes.insert('end', f'[{timestamp}] {text}\n')
|
||||
self.notes.see('end')
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ EJEMPLO DE LOG EN INTERFAZ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📝 Panel de Notas │ │
|
||||
│ ├─────────────────────────────────────────────────────────────┤ │
|
||||
│ │ [19:28:43] Conectando a 10.10.0.101:143... │ │
|
||||
│ │ [19:28:44] Conexión IMAP establecida │ │
|
||||
│ │ [19:28:44] Carpetas IMAP disponibles: ['INBOX', 'Sent'] │ │
|
||||
│ │ [19:28:44] Credenciales guardadas correctamente │ │
|
||||
│ │ [19:28:45] Cargando 8 correos... │ │
|
||||
│ │ [19:28:46] 8 correos cargados (2 sin leer) │ │
|
||||
│ │ [19:29:10] === CORREO SELECCIONADO #0: Test grabación === │ │
|
||||
│ │ [19:29:10] Correo marcado como leído en el servidor │ │
|
||||
│ │ [19:29:10] >>> Actualizando encabezados │ │
|
||||
│ │ [19:29:10] Texto plano encontrado: 245 caracteres │ │
|
||||
│ │ [19:29:10] Adjunto detectado: imagen.png (image/png) │ │
|
||||
│ │ [19:29:10] Imagen inline mostrada: imagen.png │ │
|
||||
│ │ [19:29:10] >>> _display_mail COMPLETADO OK │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📊 Resumen de Funciones Clave
|
||||
|
||||
| Función | Líneas | Responsabilidad |
|
||||
|---------|--------|-----------------|
|
||||
| `_build_tab_correos()` | 632-850 | Construir toda la interfaz del tab Correos |
|
||||
| `_load_mail_credentials()` | 1483-1525 | Cargar credenciales de `.mail_config.json` |
|
||||
| `_save_mail_credentials()` | 1527-1559 | Guardar credenciales con Base64 |
|
||||
| `_connect_mail_server()` | 1561-1613 | Conectar a IMAP, listar carpetas |
|
||||
| `_refresh_mail_list()` | 1635-1801 | Cargar correos de INBOX con FETCH |
|
||||
| `_show_inbox()` | 1803-1811 | Cambiar a bandeja de entrada |
|
||||
| `_show_sent()` | 1828-1880 | Cambiar a carpeta de enviados |
|
||||
| `_on_mail_select()` | 1982-2053 | Manejar clic en correo, marcar como leído |
|
||||
| `_display_mail()` | 2055-2337 | Mostrar contenido, imágenes y adjuntos |
|
||||
| `_open_compose_window()` | 2353-2788 | Abrir ventana de redacción |
|
||||
| `_send_mail_with_attachments()` | 2837-2977 | Enviar correo por SMTP con adjuntos |
|
||||
| `_save_to_sent_folder()` | 2979-3033 | Guardar copia en servidor IMAP |
|
||||
|
||||
---
|
||||
|
||||
### 🔄 Flujo Completo de Usuario
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ FLUJO COMPLETO: LEER Y ENVIAR CORREO │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. INICIAR APLICACIÓN │
|
||||
│ └─► python3 app.py │
|
||||
│ └─► _load_mail_credentials() precarga usuario y contraseña │
|
||||
│ │
|
||||
│ 2. IR A TAB "CORREOS" │
|
||||
│ └─► _build_tab_correos() ya construyó la interfaz │
|
||||
│ │
|
||||
│ 3. CONECTAR │
|
||||
│ └─► Clic en "🔗 Conectar" │
|
||||
│ └─► _connect_mail_server() │
|
||||
│ ├─ IMAP4('10.10.0.101', 143) │
|
||||
│ ├─ login('marcos@psp.es', '1234') │
|
||||
│ └─ list() → Muestra carpetas disponibles │
|
||||
│ └─► _save_mail_credentials() si "Recordar" está marcado │
|
||||
│ └─► _refresh_mail_list() carga correos automáticamente │
|
||||
│ │
|
||||
│ 4. LEER CORREO │
|
||||
│ └─► Clic en correo de la lista │
|
||||
│ └─► _on_mail_select() │
|
||||
│ ├─ store(id, '+FLAGS', '\\Seen') si no leído │
|
||||
│ └─ _display_mail(mail_info) │
|
||||
│ ├─ Actualiza encabezados │
|
||||
│ ├─ Muestra cuerpo │
|
||||
│ ├─ Muestra imágenes inline │
|
||||
│ └─ Muestra botones para guardar adjuntos │
|
||||
│ │
|
||||
│ 5. ENVIAR NUEVO CORREO │
|
||||
│ └─► Clic en "✉️ Nuevo correo" │
|
||||
│ └─► _open_compose_window() │
|
||||
│ ├─ Ventana emergente con campos │
|
||||
│ ├─ Botón "📎 Adjuntar archivo" │
|
||||
│ ├─ Soporte Ctrl+V para imágenes │
|
||||
│ └─ Botón "📤 ENVIAR CORREO" │
|
||||
│ ├─ Validar destinatarios (regex) │
|
||||
│ ├─ Confirmar si múltiples │
|
||||
│ ├─ _send_mail_with_attachments() │
|
||||
│ │ ├─ Crear MIMEMultipart │
|
||||
│ │ ├─ Adjuntar archivos │
|
||||
│ │ └─ SMTP send_message() │
|
||||
│ └─ _save_to_sent_folder() │
|
||||
│ └─ IMAP APPEND a 'Sent' │
|
||||
│ │
|
||||
│ 6. VERIFICAR EN WEBMIN │
|
||||
│ └─► Abrir http://10.10.0.101:20000 │
|
||||
│ └─► Carpeta "Sent" → Correo aparece ahí │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Características del Dashboard
|
||||
|
||||
### 📊 Monitor del Sistema
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
# Guía de Pruebas - Cliente de Correo Electrónico
|
||||
## Proyecto1AVApsp - Sesión del 16-19 de Febrero 2026
|
||||
|
||||
Esta guía te ayudará a probar todas las funcionalidades recién implementadas en el cliente de correo.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 CORRECCIÓN APLICADA (19 Feb 2026)
|
||||
|
||||
**Problema**: Error `AttributeError: '_tkinter.tkapp' object has no attribute 'notes'` al iniciar la aplicación
|
||||
**Causa**: La función `_load_mail_credentials()` intentaba escribir en el log antes de que el widget `notes` se hubiera creado
|
||||
**Solución**: Modificada la función `_log()` (app.py:4304) para verificar si el widget existe antes de usarlo
|
||||
**Estado**: ✅ **CORREGIDO** - La aplicación ahora arranca sin errores
|
||||
|
||||
---
|
||||
|
||||
## ✅ FUNCIONALIDADES IMPLEMENTADAS EN ESTA SESIÓN
|
||||
|
||||
### 1. **Guardado automático de credenciales**
|
||||
### 2. **Envío a múltiples destinatarios**
|
||||
### 3. **Guardado de correos enviados en el servidor IMAP**
|
||||
### 4. **Eliminación del botón "Actualizar"**
|
||||
### 5. **Corrección visual de indicadores de correos no leídos**
|
||||
|
||||
---
|
||||
|
||||
## 🧪 PLAN DE PRUEBAS
|
||||
|
||||
### PRUEBA 1: Guardado de Credenciales
|
||||
|
||||
**Objetivo**: Verificar que las credenciales se guarden y carguen automáticamente
|
||||
|
||||
**Pasos**:
|
||||
1. Ejecuta la aplicación: `python3 app.py`
|
||||
2. Ve a la pestaña "Correos"
|
||||
3. Observa si los campos están precargados con tus credenciales
|
||||
4. Si es la primera vez, introduce:
|
||||
- Usuario: `marcos@psp.es`
|
||||
- Contraseña: tu contraseña
|
||||
5. Marca la casilla "💾 Recordar credenciales" (debe estar marcada por defecto)
|
||||
6. Haz clic en "🔌 Conectar"
|
||||
7. Verifica que aparezca el mensaje "Credenciales guardadas correctamente" en el log
|
||||
8. **CIERRA completamente la aplicación**
|
||||
9. Vuelve a ejecutar: `python3 app.py`
|
||||
10. Ve a la pestaña "Correos"
|
||||
|
||||
**Resultado esperado**:
|
||||
- ✅ Los campos de usuario, contraseña y servidores deben estar precargados
|
||||
- ✅ El archivo `.mail_config.json` existe en el directorio del proyecto
|
||||
- ✅ Puedes conectarte sin volver a escribir las credenciales
|
||||
|
||||
**Para probar el borrado de credenciales**:
|
||||
1. Conecta al servidor
|
||||
2. Desconecta
|
||||
3. Desmarca "💾 Recordar credenciales"
|
||||
4. Vuelve a conectar
|
||||
5. Cierra la aplicación
|
||||
6. Al abrir de nuevo, los campos deben estar vacíos
|
||||
|
||||
---
|
||||
|
||||
### PRUEBA 2: Múltiples Destinatarios
|
||||
|
||||
**Objetivo**: Verificar que se pueden enviar correos a varios destinatarios simultáneamente
|
||||
|
||||
**Pasos**:
|
||||
1. Conecta al servidor de correo
|
||||
2. Haz clic en "✉️ Nuevo correo"
|
||||
3. En el campo "Para:", introduce **múltiples correos separados por comas**:
|
||||
```
|
||||
marcos@psp.es, destinatario2@ejemplo.com, destinatario3@ejemplo.com
|
||||
```
|
||||
4. También puedes usar **punto y coma**:
|
||||
```
|
||||
marcos@psp.es; destinatario2@ejemplo.com
|
||||
```
|
||||
5. Introduce un asunto: "Prueba múltiples destinatarios"
|
||||
6. Escribe un mensaje: "Este es un correo de prueba"
|
||||
7. Haz clic en "📤 ENVIAR CORREO"
|
||||
|
||||
**Resultado esperado**:
|
||||
- ✅ Aparece un diálogo de confirmación mostrando los 3 destinatarios
|
||||
- ✅ El mensaje dice: "¿Enviar correo a 3 destinatarios?"
|
||||
- ✅ Se listan todos los correos con un • al inicio
|
||||
- ✅ Al confirmar, el correo se envía correctamente
|
||||
- ✅ El mensaje de éxito indica "enviado a 3 destinatarios"
|
||||
|
||||
**Prueba con email inválido**:
|
||||
1. Intenta enviar a: `marcos@psp.es, correo-invalido, otro@ejemplo.com`
|
||||
2. Haz clic en "📤 ENVIAR CORREO"
|
||||
|
||||
**Resultado esperado**:
|
||||
- ✅ Aparece advertencia: "Los siguientes emails no son válidos: correo-invalido"
|
||||
- ✅ NO se envía el correo hasta que corrijas el formato
|
||||
|
||||
---
|
||||
|
||||
### PRUEBA 3: Guardado en Carpeta "Enviados" del Servidor
|
||||
|
||||
**Objetivo**: Verificar que los correos enviados se guardan en el servidor IMAP y son visibles en Webmin
|
||||
|
||||
**Pasos**:
|
||||
1. Conecta al servidor de correo en la aplicación
|
||||
2. Envía un correo de prueba:
|
||||
- **Para**: marcos@psp.es
|
||||
- **Asunto**: "Prueba guardado en servidor"
|
||||
- **Mensaje**: "Verificando que este correo se guarda en IMAP"
|
||||
3. Observa el log en la parte inferior de la aplicación
|
||||
4. Busca mensajes como:
|
||||
```
|
||||
Correo guardado en carpeta: Sent
|
||||
```
|
||||
5. Haz clic en el botón "📧 Enviados" en la aplicación
|
||||
6. **Verifica que el correo aparece en la lista**
|
||||
7. Ahora abre tu navegador y ve a: `http://10.10.0.101:20000`
|
||||
8. Inicia sesión en Webmin
|
||||
9. Ve a "Correo" → "Usermin" → "Read Email" o accede directamente a tu cliente de correo Webmin
|
||||
10. Busca la carpeta "Sent" o "Enviados"
|
||||
|
||||
**Resultado esperado**:
|
||||
- ✅ El correo aparece en la pestaña "📧 Enviados" de la aplicación
|
||||
- ✅ El correo también aparece en Webmin en la carpeta de enviados
|
||||
- ✅ Si abres el correo en Webmin, debe tener el contenido correcto
|
||||
- ✅ Los adjuntos (si los había) también están presentes
|
||||
|
||||
**Si falla**:
|
||||
- Revisa el log de la aplicación para ver qué carpeta se intentó usar
|
||||
- Las carpetas probadas automáticamente son:
|
||||
- `Sent`
|
||||
- `INBOX.Sent`
|
||||
- `Enviados`
|
||||
- `INBOX.Enviados`
|
||||
- `Sent Items`
|
||||
- Si tu servidor usa otro nombre, aparecerá un mensaje de error en el log
|
||||
|
||||
---
|
||||
|
||||
### PRUEBA 4: Interfaz sin Botón "Actualizar"
|
||||
|
||||
**Objetivo**: Verificar que la interfaz se actualiza automáticamente sin necesidad del botón
|
||||
|
||||
**Pasos**:
|
||||
1. Conecta al servidor
|
||||
2. Observa la barra de herramientas (arriba de la lista de correos)
|
||||
3. Verifica que solo hay **un botón**: "✉️ Nuevo correo"
|
||||
4. El botón "🔄 Actualizar" ya **NO debe existir**
|
||||
5. Haz clic en "📧 Enviados"
|
||||
6. Observa que la lista se actualiza automáticamente
|
||||
7. Haz clic en "📬 Entrada"
|
||||
8. Nuevamente, la lista se actualiza sin necesidad de botón
|
||||
|
||||
**Resultado esperado**:
|
||||
- ✅ No existe el botón "🔄 Actualizar"
|
||||
- ✅ La lista se actualiza automáticamente al cambiar de carpeta
|
||||
- ✅ La interfaz se ve más limpia
|
||||
|
||||
---
|
||||
|
||||
### PRUEBA 5: Indicadores Visuales de Correos No Leídos
|
||||
|
||||
**Objetivo**: Verificar que los correos no leídos se distinguen visualmente sin errores
|
||||
|
||||
**Pasos**:
|
||||
1. Conecta al servidor
|
||||
2. Ve a "📬 Entrada"
|
||||
3. Observa la lista de correos
|
||||
4. Identifica correos no leídos y leídos
|
||||
|
||||
**Resultado esperado**:
|
||||
- ✅ **Correos NO leídos**: Tienen el emoji 🔵 al inicio y texto en **negro**
|
||||
- ✅ **Correos leídos**: **NO** tienen emoji y el texto está en **gris**
|
||||
- ✅ NO aparece el error: "unknown option '-font'"
|
||||
- ✅ La lista se carga sin errores en el log
|
||||
|
||||
**Apariencia visual**:
|
||||
```
|
||||
🔵 [01/02/25] De: remitente@ejemplo.com | Asunto: Correo nuevo ← No leído
|
||||
[31/01/25] De: otro@ejemplo.com | Asunto: Correo antiguo ← Leído (gris)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 PRUEBAS ADICIONALES RECOMENDADAS
|
||||
|
||||
### Prueba de Adjuntos con Múltiples Destinatarios
|
||||
1. Envía un correo a 2-3 destinatarios
|
||||
2. Incluye 1-2 archivos adjuntos (imágenes, PDFs)
|
||||
3. Verifica que todos los destinatarios reciben los adjuntos
|
||||
|
||||
### Prueba de Imágenes Pegadas (Ctrl+V)
|
||||
1. Copia una imagen del portapapeles
|
||||
2. En la ventana de composición, presiona Ctrl+V
|
||||
3. Verifica que la imagen se muestra en la vista previa
|
||||
4. Envía el correo
|
||||
5. Verifica en "Enviados" que el correo tiene el adjunto
|
||||
|
||||
### Prueba de Longitud de Lista de Destinatarios
|
||||
1. Intenta enviar a 10+ destinatarios
|
||||
2. Verifica que el diálogo de confirmación muestra todos
|
||||
3. Confirma que se envía correctamente
|
||||
|
||||
---
|
||||
|
||||
## 🐛 QUÉ HACER SI ENCUENTRAS ERRORES
|
||||
|
||||
### Error: "No se puede guardar en carpeta Sent"
|
||||
**Solución**:
|
||||
1. Revisa el log para ver qué carpetas se intentaron
|
||||
2. Conéctate a Webmin y verifica el nombre exacto de tu carpeta de enviados
|
||||
3. Si tiene un nombre diferente, avísame para ajustar el código
|
||||
|
||||
### Error: "Los siguientes emails no son válidos"
|
||||
**Causa**: Formato incorrecto de email
|
||||
**Solución**: Verifica que todos los emails tengan el formato: `usuario@dominio.ext`
|
||||
|
||||
### Error: "unknown option '-font'"
|
||||
**Causa**: Este error ya fue corregido
|
||||
**Solución**: Si aún aparece, verifica que estás usando la versión más reciente de `app.py`
|
||||
|
||||
### Credenciales no se cargan automáticamente
|
||||
**Solución**:
|
||||
1. Verifica que el archivo `.mail_config.json` existe
|
||||
2. Ejecuta: `cat .mail_config.json` para ver su contenido
|
||||
3. Verifica que la casilla "💾 Recordar credenciales" esté marcada al conectar
|
||||
|
||||
---
|
||||
|
||||
## 📊 REGISTRO DE PRUEBAS
|
||||
|
||||
Usa esta tabla para registrar tus pruebas:
|
||||
|
||||
| Prueba | Estado | Observaciones |
|
||||
|--------|--------|---------------|
|
||||
| Guardado de credenciales | ⬜ Pendiente / ✅ OK / ❌ Error | |
|
||||
| Múltiples destinatarios | ⬜ Pendiente / ✅ OK / ❌ Error | |
|
||||
| Guardado en servidor IMAP | ⬜ Pendiente / ✅ OK / ❌ Error | |
|
||||
| Sin botón "Actualizar" | ⬜ Pendiente / ✅ OK / ❌ Error | |
|
||||
| Indicadores visuales | ⬜ Pendiente / ✅ OK / ❌ Error | |
|
||||
|
||||
---
|
||||
|
||||
## 📝 NOTAS IMPORTANTES
|
||||
|
||||
### Seguridad de Credenciales
|
||||
- Las contraseñas se guardan con codificación Base64 (NO es encriptación real)
|
||||
- Cualquiera con acceso al archivo `.mail_config.json` puede decodificar la contraseña
|
||||
- El archivo está excluido de Git para no subirlo accidentalmente
|
||||
- Para mayor seguridad en producción, considera usar `cryptography.fernet`
|
||||
|
||||
### Limitaciones Conocidas
|
||||
- El servidor IMAP/SMTP debe estar accesible en `10.10.0.101`
|
||||
- No se usa TLS/SSL (puerto 25 y 143 son sin cifrado)
|
||||
- Los correos HTML se muestran como texto plano
|
||||
- No hay soporte para CC/BCC (se puede agregar si lo necesitas)
|
||||
|
||||
---
|
||||
|
||||
## ✨ PRÓXIMAS MEJORAS SUGERIDAS
|
||||
|
||||
Si todo funciona correctamente, estas son funcionalidades que se pueden agregar:
|
||||
|
||||
1. **Campos CC y BCC** para copias de correos
|
||||
2. **Responder y Reenviar** correos existentes
|
||||
3. **Búsqueda de correos** por remitente, asunto o contenido
|
||||
4. **Firma automática** al final de cada correo
|
||||
5. **Plantillas de correo** predefinidas
|
||||
6. **Encriptación real** de contraseñas con Fernet
|
||||
7. **Soporte TLS/SSL** para conexiones seguras
|
||||
8. **Carpetas personalizadas** (Borradores, Papelera, etc.)
|
||||
9. **Exportar correos** a PDF o HTML
|
||||
10. **Notificaciones de escritorio** para correos nuevos
|
||||
|
||||
---
|
||||
|
||||
## 🚀 EJECUTAR PRUEBAS
|
||||
|
||||
Para empezar a probar, simplemente ejecuta:
|
||||
|
||||
```bash
|
||||
cd /home/marcos/Documentos/Proyecto1AVApsp
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
Y sigue las pruebas en orden del 1 al 5.
|
||||
|
||||
**¡Buena suerte con las pruebas!** Si encuentras algún problema, avísame con el mensaje de error del log.
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -5,3 +5,4 @@ pillow>=9.0.0
|
|||
pygame>=2.1.0
|
||||
requests>=2.32.0
|
||||
beautifulsoup4>=4.12.0
|
||||
# Email libraries are part of Python standard library (smtplib, imaplib, email)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de prueba para verificar la conexión con el servidor de correo Webmin
|
||||
"""
|
||||
import socket
|
||||
|
||||
def test_connection(host, port, service_name):
|
||||
"""Prueba la conexión a un puerto específico"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
result = sock.connect_ex((host, port))
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
print(f"✓ {service_name} (puerto {port}): CONECTADO")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ {service_name} (puerto {port}): NO DISPONIBLE")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ {service_name} (puerto {port}): ERROR - {e}")
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Probando conexión con servidor Webmin...")
|
||||
print("=" * 50)
|
||||
|
||||
host = '10.10.0.101'
|
||||
|
||||
# Probar puertos
|
||||
results = []
|
||||
results.append(test_connection(host, 20000, 'Webmin Web Interface'))
|
||||
results.append(test_connection(host, 25, 'SMTP'))
|
||||
results.append(test_connection(host, 143, 'IMAP'))
|
||||
results.append(test_connection(host, 110, 'POP3'))
|
||||
|
||||
print("=" * 50)
|
||||
if all(results[1:]): # Ignorar webmin interface para el resultado
|
||||
print("✓ Todos los servicios de correo están disponibles")
|
||||
else:
|
||||
print("⚠ Algunos servicios de correo no están disponibles")
|
||||
print("\nSugerencias:")
|
||||
print("1. Verifica que el servidor Webmin esté en ejecución")
|
||||
print("2. Comprueba la configuración del firewall")
|
||||
print("3. Verifica que los servicios de correo estén habilitados en Webmin")
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de prueba para verificar que la aplicación arranca correctamente
|
||||
sin errores relacionados con el widget notes.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
def test_startup():
|
||||
"""Prueba que la aplicación arranca sin errores críticos"""
|
||||
print("🧪 Probando inicio de aplicación...")
|
||||
print("=" * 60)
|
||||
|
||||
# Ejecutar la aplicación por 3 segundos y capturar salida
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
['python3', 'app.py'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Esperar 3 segundos para ver si hay errores iniciales
|
||||
time.sleep(3)
|
||||
|
||||
# Intentar terminar el proceso
|
||||
process.terminate()
|
||||
|
||||
# Esperar a que termine y capturar salida
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
print("\n📤 Salida estándar:")
|
||||
print("-" * 60)
|
||||
if stdout:
|
||||
print(stdout)
|
||||
else:
|
||||
print("(Sin salida)")
|
||||
|
||||
print("\n⚠️ Errores/Advertencias:")
|
||||
print("-" * 60)
|
||||
if stderr:
|
||||
# Filtrar errores críticos
|
||||
lines = stderr.split('\n')
|
||||
critical_errors = [line for line in lines if 'AttributeError' in line or 'Traceback' in line or 'Error' in line]
|
||||
|
||||
if critical_errors:
|
||||
print("❌ ERRORES CRÍTICOS ENCONTRADOS:")
|
||||
for line in critical_errors:
|
||||
print(f" {line}")
|
||||
return False
|
||||
else:
|
||||
print("(Solo advertencias menores, no errores críticos)")
|
||||
else:
|
||||
print("(Sin errores)")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ La aplicación arrancó correctamente")
|
||||
print("✅ No se detectaron errores de AttributeError con 'notes'")
|
||||
print("✅ Puedes ejecutar: python3 app.py")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error durante la prueba: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = test_startup()
|
||||
sys.exit(0 if success else 1)
|
||||
Loading…
Reference in New Issue