Metadata-Version: 2.4
Name: YurMail
Version: 0.2.0
Summary: Async email scheduling, drafting, and Microsoft Graph mail integration.
Author: José Luis
License: MIT
Keywords: email,scheduler,microsoft-graph,outlook,asyncio,openai,drafts
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: AsyncIO
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: aiohttp>=3.9
Requires-Dist: openai>=1.30
Requires-Dist: python-dotenv>=1.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: build>=1.2.1; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"

# YurMail

Librería Python para manejar correo personal de Outlook/Hotmail via Microsoft Graph API. Permite enviar, programar, y generar correos con y sin IA, sin necesidad de manejar HTML ni credenciales hardcodeadas.

```python
from mail_scheduler import GraphAuthManager, GraphMailClient, text_to_html

auth   = GraphAuthManager(client_id="TU_CLIENT_ID", ...)
tokens = await auth.login()
mail   = GraphMailClient(tokens["access_token"])

await mail.send_mail(
    to=["amigo@outlook.com"],
    subject="Hola",
    body=text_to_html("Hola,\n\nTe escribo para confirmar la reunión.\n\nSaludos"),
)
```

---

## Prerequisitos

### 1. Cuenta Microsoft personal

Necesitas una cuenta `@outlook.com` o `@hotmail.com`. Las cuentas institucionales (universitarias, empresariales) están administradas por un tercero y pueden requerir aprobación de su área de IT.

### 2. Registrar una aplicación en Microsoft Entra

Este paso es **obligatorio y manual**. Solo lo haces una vez. Microsoft requiere que cualquier aplicación que acceda a tu correo esté registrada.

1. Ve a [entra.microsoft.com](https://entra.microsoft.com) e inicia sesión con tu cuenta personal
2. Ve a **Applications → App registrations → New registration**
3. Llena el formulario:
   - **Name:** el nombre que quieras (ej. `mi-mail-scheduler`)
   - **Supported account types:** `Personal Microsoft accounts only`
   - **Redirect URI:** selecciona plataforma **Mobile and desktop applications** y escribe `http://localhost:8765/callback`
4. Haz clic en **Register**
5. Copia el **Application (client) ID** — lo necesitarás después

> Elige la plataforma **"Mobile and desktop applications"**, no "Web". De lo contrario Microsoft exigirá un `client_secret` que no necesitas con esta librería.

### 3. Configurar permisos

En tu app registration recién creada:

1. Ve a **API Permissions → Add a permission → Microsoft Graph → Delegated**
2. Agrega estos permisos:

| Permiso | Para qué |
|---|---|
| `offline_access` | Renovar el token sin pedir login cada hora |
| `User.Read` | Obtener el perfil del usuario |
| `Mail.ReadWrite` | Leer, mover y marcar mensajes |
| `Mail.Send` | Enviar y responder correos |

3. Haz clic en **Add permissions**

No necesitas `client_secret` — la librería usa PKCE, el estándar para aplicaciones públicas.

---

## Instalación

```bash
pip install -r requirements.txt
```

---

## Configuración

Crea un archivo `.env` en tu proyecto (nunca lo subas a git):

```env
MAILSCHEDULER_CLIENT_ID=tu-application-client-id
MAILSCHEDULER_TENANT=consumers
OPENAI_API_KEY=api-key
```

---

## Uso básico

### Login

```python
import asyncio
from dotenv import load_dotenv
import os
from YurMail import GraphAuthManager, GraphMailClient

load_dotenv()

async def main():
    auth = GraphAuthManager(
        client_id=os.getenv("MAILSCHEDULER_CLIENT_ID"),
        tenant="consumers",
        redirect_uri="http://localhost:8765/callback",
        scopes=["offline_access", "User.Read", "Mail.ReadWrite", "Mail.Send"],
    )

    # Abre el browser una vez. El usuario inicia sesión y da consentimiento.
    tokens = await auth.login()
    mail   = GraphMailClient(tokens["access_token"])

asyncio.run(main())
```

La primera vez que corras `login()` Microsoft te mostrará una pantalla de consentimiento donde aceptas los permisos. Las siguientes veces usará el `refresh_token` para renovar el acceso sin abrir el browser.

---

### Enviar un correo sin HTML

```python
from mail_scheduler import text_to_html

await mail.send_mail(
    to=["destinatario@outlook.com"],
    subject="Reunión del viernes",
    body=text_to_html("""
        Hola Carlos,

        Te confirmo la reunión del viernes a las 10 AM.
        Será en la sala de juntas del tercer piso.

        Saludos,
        Ana
    """),
)
```

### Enviar con adjunto

```python
from mail_scheduler import attachment_from_bytes
from pathlib import Path

# Desde un archivo en disco
await mail.send_mail(
    to=["jefe@outlook.com"],
    subject="Reporte",
    body=text_to_html("Adjunto el reporte de esta semana."),
    attachments=["reporte.pdf"],
)

# Desde bytes en memoria (sin escribir a disco)
csv_bytes = generar_csv()
await mail.send_mail(
    to=["jefe@outlook.com"],
    subject="Datos",
    body=text_to_html("Adjunto los datos."),
    attachments=[attachment_from_bytes(csv_bytes, "datos.csv", "text/csv")],
)
```

### Usar templates

```python
from mail_scheduler import TEMPLATE_REPORTE, MailTemplate

# Template predefinido
subject, body = TEMPLATE_REPORTE.render(
    nombre="Gerente",
    periodo="Abril 2026",
    titulo="Ventas Q2",
    resumen="Crecimiento del 12% respecto al mes anterior.",
)
await mail.send_mail(to=["gerente@outlook.com"], subject=subject, body=body)

# Template propio
aviso = MailTemplate(
    name="aviso",
    subject="Aviso: {{ titulo }}",
    body="<p>Hola {{ nombre }},</p><p>{{ mensaje }}</p>",
)
subject, body = aviso.render(titulo="Cambio de horario", nombre="Equipo", mensaje="El lunes no hay clases.")
```

### Guardar como borrador (para revisar antes de enviar)

```python
draft = await mail.create_draft(
    to=["cliente@outlook.com"],
    subject="Propuesta comercial",
    body=text_to_html("Estimado cliente,\n\nAdjunto nuestra propuesta."),
)
# Aparece en tu carpeta Drafts de Outlook para revisarlo antes de enviarlo
print(f"Borrador guardado: {draft.id}")

# Cuando estés listo:
await mail.send_draft(draft.id)
```

### Scheduler con IA

```python
from YurMail import OpenAIDraftClient, Scheduler, InMemoryStore, EmailBuilder
from datetime import datetime, timedelta, timezone

llm       = OpenAIDraftClient(api_key=os.getenv("OPENAI_API_KEY"))
scheduler = Scheduler(mail_client=mail, llm_client=llm, store=InMemoryStore())

# Programa un correo: la IA genera el contenido justo antes de enviarlo
email = (
    EmailBuilder()
    .purpose("Recordar al equipo la entrega del proyecto del viernes")
    .to(["equipo@outlook.com"])
    .recipient_name("Equipo")
    .tone("casual")
    .language("es")
    .send_at(datetime.now(timezone.utc) + timedelta(hours=2))
    .build()
)
scheduler.schedule(email)
await scheduler.run_once()
```

### Scheduler → borrador (revisar antes de enviar)

```python
email = (
    EmailBuilder()
    .purpose("Responder al cliente sobre el retraso en la entrega")
    .to(["cliente@outlook.com"])
    .tone("formal")
    .send_at(datetime.now(timezone.utc) + timedelta(minutes=5))
    .save_as_draft()   # la IA genera el contenido pero va a Drafts
    .build()
)
scheduler.schedule(email)
await scheduler.run_once()
# Revisa tu carpeta Drafts, edita si necesitas, y envía desde Outlook
```

---

## Referencia rápida

| Función | Para qué |
|---|---|
| `GraphAuthManager.login()` | Login OAuth con browser |
| `GraphAuthManager.refresh_access_token(token)` | Renovar token expirado |
| `GraphMailClient.get_me()` | Perfil del usuario autenticado |
| `GraphMailClient.list_messages(top, folder, only_unread)` | Listar mensajes |
| `GraphMailClient.get_message(id)` | Mensaje completo con body HTML |
| `GraphMailClient.send_mail(to, subject, body, ...)` | Enviar correo |
| `GraphMailClient.create_draft(to, subject, body, ...)` | Guardar borrador |
| `GraphMailClient.send_draft(draft_id)` | Enviar borrador existente |
| `GraphMailClient.reply_to_message(id, body)` | Responder manteniendo hilo |
| `GraphMailClient.mark_as_read(id)` | Marcar como leído |
| `GraphMailClient.move_to_trash(id)` | Mover a eliminados |
| `text_to_html(texto)` | Convertir texto plano a HTML |
| `attachment_from_bytes(data, filename)` | Adjunto desde memoria |
| `MailTemplate.render(**kwargs)` | Renderizar template |
| `EmailBuilder().purpose().to().send_at().build()` | Construir correo programado |
| `Scheduler.schedule(email)` | Agregar correo al scheduler |
| `Scheduler.run_once()` | Procesar correos vencidos |
| `Scheduler.run_loop(interval_seconds)` | Bucle continuo |

---

## Estructura del proyecto

```
YurMail/
├── authentication_microsoft/   OAuth2 + PKCE contra Microsoft
├── mail/                       Cliente Graph + templates + adjuntos
├── LLM/                        Generación de borradores con OpenAI
└── scheduler/                  Programación y recurrencia de envíos
```

---

## Notas

- El `access_token` expira en **1 hora**. Usa `refresh_access_token()` para renovarlo sin pedir login de nuevo.
- El `refresh_token` expira en **90 días sin uso**. Si expira, el usuario debe hacer `login()` de nuevo.
- El `CLIENT_ID` no es un secreto — puedes compartirlo. Lo que nunca debes compartir son los tokens.
- Esta librería es solo para cuentas **personales** de Microsoft (`@outlook.com`, `@hotmail.com`). Para cuentas corporativas el proceso de registro es diferente.

## Tutorial interactivo

Prueba YurMail directamente en tu navegador sin instalar nada:

[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Joselmi/MiLibreria/blob/main/tutorial_yurmail.ipynb)
