# outlook_utilities.py — Microsoft Graph (Outlook) Cliente mínimo de Microsoft Graph para leer la bandeja de entrada, bajar adjuntos y enviar las notificaciones. Autenticación con MSAL (client credentials, permisos de aplicación). Cómo obtener las credenciales: [configurar_msal.md](configurar_msal.md). ## Estructura ``` outlook_utilities.py ├── CONFIG (.env): CLIENT_ID, CLIENT_SECRET, TENANT_ID, VERIFY_SSL (false por defecto) ├── get_access_token() → str ├── get_emails(user_email, token, fecha_inicio, fecha_fin) → list[dict] ├── download_attachments(user_email, message_id, token, download_path) → list[str] └── send_email(access_token, from_email, to_recipients, subject, html_body, attachment_paths=None, cc_recipients=None) → bool ``` ## Funciones | Función | Endpoint Graph | Qué hace | |---------|----------------|----------| | `get_access_token` | `login.microsoftonline.com/{TENANT_ID}` (scope `.default`) | Obtiene el token de aplicación. Lanza `Exception` si MSAL devuelve error. | | `get_emails` | `GET /users/{user}/mailFolders/Inbox/messages` | Correos de la bandeja de entrada del rango, con paginación (`$top=50`, sigue `@odata.nextLink`). Devuelve `id`, `fecha`, `sender`, `subject`, `has_attach` y `es_automatico` (calculado con los encabezados del mensaje). Lanza `Exception` si el status no es 200. | | `download_attachments` | `GET /users/{user}/messages/{id}/attachments` | Guarda en `download_path` los adjuntos tipo archivo y devuelve sus nombres. Crea la carpeta si no existe. | | `send_email` | `POST /users/{from}/sendMail` | Envía HTML con adjuntos, CC y copia oculta (`bcc_recipients`); guarda copia en Enviados. Devuelve `True` si Graph responde 202; `False` (con `ERROR` en el log) en cualquier otro caso. No lanza excepciones. | ## Zona horaria `get_emails` asume UTC-5 (Perú, sin horario de verano): el rango `fecha_inicio`–`fecha_fin` se convierte a UTC sumando 5 h (el fin incluye todo el día), y el campo `fecha` devuelto resta 5 h a `receivedDateTime`. ## Adjuntos de `send_email` Los adjuntos se leen de las rutas indicadas. Una ruta inexistente se omite con un `WARNING`. El `contentType` se deduce de la extensión (`.pdf`, `.xml`, `.json`; cualquier otra, `application/octet-stream`). ## Notas - `download_attachments` solo guarda adjuntos tipo `fileAttachment` con contenido; los adjuntos de tipo mensaje o referencia se ignoran. - Dos correos con un adjunto del mismo nombre se sobrescriben entre sí si comparten `download_path` (el pipeline usa una única carpeta, `DOWNLOAD_FOLDER`). - `VERIFY_SSL` se respeta en `get_access_token`, `get_emails` y `download_attachments`, pero `send_email` tiene `verify_ssl = False` fijo y lo ignora. - El módulo ejecuta `warnings.filterwarnings("ignore")`, lo que silencia todas las advertencias de Python del proceso (incluidas las de certificado SSL). - `get_access_token` retorna `None` (sin lanzar error) si MSAL devuelve un resultado vacío. - `es_mensaje_automatico(headers)` marca como automáticos los mensajes con `Auto-Submitted` distinto de `no`, `X-Auto-Response-Suppress`, `X-Autoreply`, `Precedence` (`bulk`, `junk`, `list`, `auto_reply`) o `Content-Type` de informe de entrega (rebotes). El sistema no responde a esos mensajes.