YurMail 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yurmail-0.1.0/PKG-INFO +308 -0
- yurmail-0.1.0/README.md +280 -0
- yurmail-0.1.0/YurMail/LLM/__init__.py +4 -0
- yurmail-0.1.0/YurMail/LLM/client.py +73 -0
- yurmail-0.1.0/YurMail/LLM/prompts.py +46 -0
- yurmail-0.1.0/YurMail/LLM/types.py +7 -0
- yurmail-0.1.0/YurMail/__init__.py +61 -0
- yurmail-0.1.0/YurMail/authentication_microsoft/__init__.py +3 -0
- yurmail-0.1.0/YurMail/authentication_microsoft/authentication.py +260 -0
- yurmail-0.1.0/YurMail/mail/__init__.py +28 -0
- yurmail-0.1.0/YurMail/mail/client.py +505 -0
- yurmail-0.1.0/YurMail/mail/templates.py +338 -0
- yurmail-0.1.0/YurMail/scheduler/__init__.py +19 -0
- yurmail-0.1.0/YurMail/scheduler/builder.py +182 -0
- yurmail-0.1.0/YurMail/scheduler/scheduler.py +353 -0
- yurmail-0.1.0/YurMail/scheduler/store.py +205 -0
- yurmail-0.1.0/YurMail/scheduler/types.py +128 -0
- yurmail-0.1.0/YurMail.egg-info/PKG-INFO +308 -0
- yurmail-0.1.0/YurMail.egg-info/SOURCES.txt +25 -0
- yurmail-0.1.0/YurMail.egg-info/dependency_links.txt +1 -0
- yurmail-0.1.0/YurMail.egg-info/requires.txt +9 -0
- yurmail-0.1.0/YurMail.egg-info/top_level.txt +1 -0
- yurmail-0.1.0/pyproject.toml +60 -0
- yurmail-0.1.0/setup.cfg +4 -0
- yurmail-0.1.0/tests/test_authentication.py +261 -0
- yurmail-0.1.0/tests/test_client.py +388 -0
- yurmail-0.1.0/tests/test_scheduler.py +402 -0
yurmail-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: YurMail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async email scheduling, drafting, and Microsoft Graph mail integration.
|
|
5
|
+
Author: José Luis
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: email,scheduler,microsoft-graph,outlook,asyncio,openai,drafts
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Topic :: Communications :: Email
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: aiohttp>=3.9
|
|
21
|
+
Requires-Dist: openai>=1.30
|
|
22
|
+
Requires-Dist: python-dotenv>=1.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
26
|
+
Requires-Dist: build>=1.2.1; extra == "dev"
|
|
27
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# MailPilot
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from mail_scheduler import GraphAuthManager, GraphMailClient, text_to_html
|
|
35
|
+
|
|
36
|
+
auth = GraphAuthManager(client_id="TU_CLIENT_ID", ...)
|
|
37
|
+
tokens = await auth.login()
|
|
38
|
+
mail = GraphMailClient(tokens["access_token"])
|
|
39
|
+
|
|
40
|
+
await mail.send_mail(
|
|
41
|
+
to=["amigo@outlook.com"],
|
|
42
|
+
subject="Hola",
|
|
43
|
+
body=text_to_html("Hola,\n\nTe escribo para confirmar la reunión.\n\nSaludos"),
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Prerequisitos
|
|
50
|
+
|
|
51
|
+
### 1. Cuenta Microsoft personal
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
### 2. Registrar una aplicación en Microsoft Entra
|
|
56
|
+
|
|
57
|
+
Este paso es **obligatorio y manual**. Solo lo haces una vez. Microsoft requiere que cualquier aplicación que acceda a tu correo esté registrada.
|
|
58
|
+
|
|
59
|
+
1. Ve a [entra.microsoft.com](https://entra.microsoft.com) e inicia sesión con tu cuenta personal
|
|
60
|
+
2. Ve a **Applications → App registrations → New registration**
|
|
61
|
+
3. Llena el formulario:
|
|
62
|
+
- **Name:** el nombre que quieras (ej. `mi-mail-scheduler`)
|
|
63
|
+
- **Supported account types:** `Personal Microsoft accounts only`
|
|
64
|
+
- **Redirect URI:** selecciona plataforma **Mobile and desktop applications** y escribe `http://localhost:8765/callback`
|
|
65
|
+
4. Haz clic en **Register**
|
|
66
|
+
5. Copia el **Application (client) ID** — lo necesitarás después
|
|
67
|
+
|
|
68
|
+
> Elige la plataforma **"Mobile and desktop applications"**, no "Web". De lo contrario Microsoft exigirá un `client_secret` que no necesitas con esta librería.
|
|
69
|
+
|
|
70
|
+
### 3. Configurar permisos
|
|
71
|
+
|
|
72
|
+
En tu app registration recién creada:
|
|
73
|
+
|
|
74
|
+
1. Ve a **API Permissions → Add a permission → Microsoft Graph → Delegated**
|
|
75
|
+
2. Agrega estos permisos:
|
|
76
|
+
|
|
77
|
+
| Permiso | Para qué |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `offline_access` | Renovar el token sin pedir login cada hora |
|
|
80
|
+
| `User.Read` | Obtener el perfil del usuario |
|
|
81
|
+
| `Mail.ReadWrite` | Leer, mover y marcar mensajes |
|
|
82
|
+
| `Mail.Send` | Enviar y responder correos |
|
|
83
|
+
|
|
84
|
+
3. Haz clic en **Add permissions**
|
|
85
|
+
|
|
86
|
+
No necesitas `client_secret` — la librería usa PKCE, el estándar para aplicaciones públicas.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Instalación
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -r requirements.txt
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Configuración
|
|
99
|
+
|
|
100
|
+
Crea un archivo `.env` en tu proyecto (nunca lo subas a git):
|
|
101
|
+
|
|
102
|
+
```env
|
|
103
|
+
MAILSCHEDULER_CLIENT_ID=tu-application-client-id
|
|
104
|
+
MAILSCHEDULER_TENANT=consumers
|
|
105
|
+
OPENAI_API_KEY=api-key
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Uso básico
|
|
111
|
+
|
|
112
|
+
### Login
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import asyncio
|
|
116
|
+
from dotenv import load_dotenv
|
|
117
|
+
import os
|
|
118
|
+
from mail_scheduler import GraphAuthManager, GraphMailClient
|
|
119
|
+
|
|
120
|
+
load_dotenv()
|
|
121
|
+
|
|
122
|
+
async def main():
|
|
123
|
+
auth = GraphAuthManager(
|
|
124
|
+
client_id=os.getenv("MAILSCHEDULER_CLIENT_ID"),
|
|
125
|
+
tenant="consumers",
|
|
126
|
+
redirect_uri="http://localhost:8765/callback",
|
|
127
|
+
scopes=["offline_access", "User.Read", "Mail.ReadWrite", "Mail.Send"],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Abre el browser una vez. El usuario inicia sesión y da consentimiento.
|
|
131
|
+
tokens = await auth.login()
|
|
132
|
+
mail = GraphMailClient(tokens["access_token"])
|
|
133
|
+
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Enviar un correo sin HTML
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from mail_scheduler import text_to_html
|
|
145
|
+
|
|
146
|
+
await mail.send_mail(
|
|
147
|
+
to=["destinatario@outlook.com"],
|
|
148
|
+
subject="Reunión del viernes",
|
|
149
|
+
body=text_to_html("""
|
|
150
|
+
Hola Carlos,
|
|
151
|
+
|
|
152
|
+
Te confirmo la reunión del viernes a las 10 AM.
|
|
153
|
+
Será en la sala de juntas del tercer piso.
|
|
154
|
+
|
|
155
|
+
Saludos,
|
|
156
|
+
Ana
|
|
157
|
+
"""),
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Enviar con adjunto
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from mail_scheduler import attachment_from_bytes
|
|
165
|
+
from pathlib import Path
|
|
166
|
+
|
|
167
|
+
# Desde un archivo en disco
|
|
168
|
+
await mail.send_mail(
|
|
169
|
+
to=["jefe@outlook.com"],
|
|
170
|
+
subject="Reporte",
|
|
171
|
+
body=text_to_html("Adjunto el reporte de esta semana."),
|
|
172
|
+
attachments=["reporte.pdf"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Desde bytes en memoria (sin escribir a disco)
|
|
176
|
+
csv_bytes = generar_csv()
|
|
177
|
+
await mail.send_mail(
|
|
178
|
+
to=["jefe@outlook.com"],
|
|
179
|
+
subject="Datos",
|
|
180
|
+
body=text_to_html("Adjunto los datos."),
|
|
181
|
+
attachments=[attachment_from_bytes(csv_bytes, "datos.csv", "text/csv")],
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Usar templates
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from mail_scheduler import TEMPLATE_REPORTE, MailTemplate
|
|
189
|
+
|
|
190
|
+
# Template predefinido
|
|
191
|
+
subject, body = TEMPLATE_REPORTE.render(
|
|
192
|
+
nombre="Gerente",
|
|
193
|
+
periodo="Abril 2026",
|
|
194
|
+
titulo="Ventas Q2",
|
|
195
|
+
resumen="Crecimiento del 12% respecto al mes anterior.",
|
|
196
|
+
)
|
|
197
|
+
await mail.send_mail(to=["gerente@outlook.com"], subject=subject, body=body)
|
|
198
|
+
|
|
199
|
+
# Template propio
|
|
200
|
+
aviso = MailTemplate(
|
|
201
|
+
name="aviso",
|
|
202
|
+
subject="Aviso: {{ titulo }}",
|
|
203
|
+
body="<p>Hola {{ nombre }},</p><p>{{ mensaje }}</p>",
|
|
204
|
+
)
|
|
205
|
+
subject, body = aviso.render(titulo="Cambio de horario", nombre="Equipo", mensaje="El lunes no hay clases.")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Guardar como borrador (para revisar antes de enviar)
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
draft = await mail.create_draft(
|
|
212
|
+
to=["cliente@outlook.com"],
|
|
213
|
+
subject="Propuesta comercial",
|
|
214
|
+
body=text_to_html("Estimado cliente,\n\nAdjunto nuestra propuesta."),
|
|
215
|
+
)
|
|
216
|
+
# Aparece en tu carpeta Drafts de Outlook para revisarlo antes de enviarlo
|
|
217
|
+
print(f"Borrador guardado: {draft.id}")
|
|
218
|
+
|
|
219
|
+
# Cuando estés listo:
|
|
220
|
+
await mail.send_draft(draft.id)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Scheduler con IA
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from mail_scheduler import OpenAIDraftClient, Scheduler, InMemoryStore, EmailBuilder
|
|
227
|
+
from datetime import datetime, timedelta, timezone
|
|
228
|
+
|
|
229
|
+
llm = OpenAIDraftClient(api_key=os.getenv("OPENAI_API_KEY"))
|
|
230
|
+
scheduler = Scheduler(mail_client=mail, llm_client=llm, store=InMemoryStore())
|
|
231
|
+
|
|
232
|
+
# Programa un correo: la IA genera el contenido justo antes de enviarlo
|
|
233
|
+
email = (
|
|
234
|
+
EmailBuilder()
|
|
235
|
+
.purpose("Recordar al equipo la entrega del proyecto del viernes")
|
|
236
|
+
.to(["equipo@outlook.com"])
|
|
237
|
+
.recipient_name("Equipo")
|
|
238
|
+
.tone("casual")
|
|
239
|
+
.language("es")
|
|
240
|
+
.send_at(datetime.now(timezone.utc) + timedelta(hours=2))
|
|
241
|
+
.build()
|
|
242
|
+
)
|
|
243
|
+
scheduler.schedule(email)
|
|
244
|
+
await scheduler.run_once()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Scheduler → borrador (revisar antes de enviar)
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
email = (
|
|
251
|
+
EmailBuilder()
|
|
252
|
+
.purpose("Responder al cliente sobre el retraso en la entrega")
|
|
253
|
+
.to(["cliente@outlook.com"])
|
|
254
|
+
.tone("formal")
|
|
255
|
+
.send_at(datetime.now(timezone.utc) + timedelta(minutes=5))
|
|
256
|
+
.save_as_draft() # la IA genera el contenido pero va a Drafts
|
|
257
|
+
.build()
|
|
258
|
+
)
|
|
259
|
+
scheduler.schedule(email)
|
|
260
|
+
await scheduler.run_once()
|
|
261
|
+
# Revisa tu carpeta Drafts, edita si necesitas, y envía desde Outlook
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Referencia rápida
|
|
267
|
+
|
|
268
|
+
| Función | Para qué |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `GraphAuthManager.login()` | Login OAuth con browser |
|
|
271
|
+
| `GraphAuthManager.refresh_access_token(token)` | Renovar token expirado |
|
|
272
|
+
| `GraphMailClient.get_me()` | Perfil del usuario autenticado |
|
|
273
|
+
| `GraphMailClient.list_messages(top, folder, only_unread)` | Listar mensajes |
|
|
274
|
+
| `GraphMailClient.get_message(id)` | Mensaje completo con body HTML |
|
|
275
|
+
| `GraphMailClient.send_mail(to, subject, body, ...)` | Enviar correo |
|
|
276
|
+
| `GraphMailClient.create_draft(to, subject, body, ...)` | Guardar borrador |
|
|
277
|
+
| `GraphMailClient.send_draft(draft_id)` | Enviar borrador existente |
|
|
278
|
+
| `GraphMailClient.reply_to_message(id, body)` | Responder manteniendo hilo |
|
|
279
|
+
| `GraphMailClient.mark_as_read(id)` | Marcar como leído |
|
|
280
|
+
| `GraphMailClient.move_to_trash(id)` | Mover a eliminados |
|
|
281
|
+
| `text_to_html(texto)` | Convertir texto plano a HTML |
|
|
282
|
+
| `attachment_from_bytes(data, filename)` | Adjunto desde memoria |
|
|
283
|
+
| `MailTemplate.render(**kwargs)` | Renderizar template |
|
|
284
|
+
| `EmailBuilder().purpose().to().send_at().build()` | Construir correo programado |
|
|
285
|
+
| `Scheduler.schedule(email)` | Agregar correo al scheduler |
|
|
286
|
+
| `Scheduler.run_once()` | Procesar correos vencidos |
|
|
287
|
+
| `Scheduler.run_loop(interval_seconds)` | Bucle continuo |
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Estructura del proyecto
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
mail_scheduler/
|
|
295
|
+
├── authentication_microsoft/ OAuth2 + PKCE contra Microsoft
|
|
296
|
+
├── mail/ Cliente Graph + templates + adjuntos
|
|
297
|
+
├── LLM/ Generación de borradores con OpenAI
|
|
298
|
+
└── scheduler/ Programación y recurrencia de envíos
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Notas
|
|
304
|
+
|
|
305
|
+
- El `access_token` expira en **1 hora**. Usa `refresh_access_token()` para renovarlo sin pedir login de nuevo.
|
|
306
|
+
- El `refresh_token` expira en **90 días sin uso**. Si expira, el usuario debe hacer `login()` de nuevo.
|
|
307
|
+
- El `CLIENT_ID` no es un secreto — puedes compartirlo. Lo que nunca debes compartir son los tokens.
|
|
308
|
+
- Esta librería es solo para cuentas **personales** de Microsoft (`@outlook.com`, `@hotmail.com`). Para cuentas corporativas el proceso de registro es diferente.
|
yurmail-0.1.0/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# MailPilot
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from mail_scheduler import GraphAuthManager, GraphMailClient, text_to_html
|
|
7
|
+
|
|
8
|
+
auth = GraphAuthManager(client_id="TU_CLIENT_ID", ...)
|
|
9
|
+
tokens = await auth.login()
|
|
10
|
+
mail = GraphMailClient(tokens["access_token"])
|
|
11
|
+
|
|
12
|
+
await mail.send_mail(
|
|
13
|
+
to=["amigo@outlook.com"],
|
|
14
|
+
subject="Hola",
|
|
15
|
+
body=text_to_html("Hola,\n\nTe escribo para confirmar la reunión.\n\nSaludos"),
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Prerequisitos
|
|
22
|
+
|
|
23
|
+
### 1. Cuenta Microsoft personal
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
### 2. Registrar una aplicación en Microsoft Entra
|
|
28
|
+
|
|
29
|
+
Este paso es **obligatorio y manual**. Solo lo haces una vez. Microsoft requiere que cualquier aplicación que acceda a tu correo esté registrada.
|
|
30
|
+
|
|
31
|
+
1. Ve a [entra.microsoft.com](https://entra.microsoft.com) e inicia sesión con tu cuenta personal
|
|
32
|
+
2. Ve a **Applications → App registrations → New registration**
|
|
33
|
+
3. Llena el formulario:
|
|
34
|
+
- **Name:** el nombre que quieras (ej. `mi-mail-scheduler`)
|
|
35
|
+
- **Supported account types:** `Personal Microsoft accounts only`
|
|
36
|
+
- **Redirect URI:** selecciona plataforma **Mobile and desktop applications** y escribe `http://localhost:8765/callback`
|
|
37
|
+
4. Haz clic en **Register**
|
|
38
|
+
5. Copia el **Application (client) ID** — lo necesitarás después
|
|
39
|
+
|
|
40
|
+
> Elige la plataforma **"Mobile and desktop applications"**, no "Web". De lo contrario Microsoft exigirá un `client_secret` que no necesitas con esta librería.
|
|
41
|
+
|
|
42
|
+
### 3. Configurar permisos
|
|
43
|
+
|
|
44
|
+
En tu app registration recién creada:
|
|
45
|
+
|
|
46
|
+
1. Ve a **API Permissions → Add a permission → Microsoft Graph → Delegated**
|
|
47
|
+
2. Agrega estos permisos:
|
|
48
|
+
|
|
49
|
+
| Permiso | Para qué |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `offline_access` | Renovar el token sin pedir login cada hora |
|
|
52
|
+
| `User.Read` | Obtener el perfil del usuario |
|
|
53
|
+
| `Mail.ReadWrite` | Leer, mover y marcar mensajes |
|
|
54
|
+
| `Mail.Send` | Enviar y responder correos |
|
|
55
|
+
|
|
56
|
+
3. Haz clic en **Add permissions**
|
|
57
|
+
|
|
58
|
+
No necesitas `client_secret` — la librería usa PKCE, el estándar para aplicaciones públicas.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Instalación
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install -r requirements.txt
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Configuración
|
|
71
|
+
|
|
72
|
+
Crea un archivo `.env` en tu proyecto (nunca lo subas a git):
|
|
73
|
+
|
|
74
|
+
```env
|
|
75
|
+
MAILSCHEDULER_CLIENT_ID=tu-application-client-id
|
|
76
|
+
MAILSCHEDULER_TENANT=consumers
|
|
77
|
+
OPENAI_API_KEY=api-key
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Uso básico
|
|
83
|
+
|
|
84
|
+
### Login
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from dotenv import load_dotenv
|
|
89
|
+
import os
|
|
90
|
+
from mail_scheduler import GraphAuthManager, GraphMailClient
|
|
91
|
+
|
|
92
|
+
load_dotenv()
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
auth = GraphAuthManager(
|
|
96
|
+
client_id=os.getenv("MAILSCHEDULER_CLIENT_ID"),
|
|
97
|
+
tenant="consumers",
|
|
98
|
+
redirect_uri="http://localhost:8765/callback",
|
|
99
|
+
scopes=["offline_access", "User.Read", "Mail.ReadWrite", "Mail.Send"],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Abre el browser una vez. El usuario inicia sesión y da consentimiento.
|
|
103
|
+
tokens = await auth.login()
|
|
104
|
+
mail = GraphMailClient(tokens["access_token"])
|
|
105
|
+
|
|
106
|
+
asyncio.run(main())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Enviar un correo sin HTML
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from mail_scheduler import text_to_html
|
|
117
|
+
|
|
118
|
+
await mail.send_mail(
|
|
119
|
+
to=["destinatario@outlook.com"],
|
|
120
|
+
subject="Reunión del viernes",
|
|
121
|
+
body=text_to_html("""
|
|
122
|
+
Hola Carlos,
|
|
123
|
+
|
|
124
|
+
Te confirmo la reunión del viernes a las 10 AM.
|
|
125
|
+
Será en la sala de juntas del tercer piso.
|
|
126
|
+
|
|
127
|
+
Saludos,
|
|
128
|
+
Ana
|
|
129
|
+
"""),
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Enviar con adjunto
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from mail_scheduler import attachment_from_bytes
|
|
137
|
+
from pathlib import Path
|
|
138
|
+
|
|
139
|
+
# Desde un archivo en disco
|
|
140
|
+
await mail.send_mail(
|
|
141
|
+
to=["jefe@outlook.com"],
|
|
142
|
+
subject="Reporte",
|
|
143
|
+
body=text_to_html("Adjunto el reporte de esta semana."),
|
|
144
|
+
attachments=["reporte.pdf"],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Desde bytes en memoria (sin escribir a disco)
|
|
148
|
+
csv_bytes = generar_csv()
|
|
149
|
+
await mail.send_mail(
|
|
150
|
+
to=["jefe@outlook.com"],
|
|
151
|
+
subject="Datos",
|
|
152
|
+
body=text_to_html("Adjunto los datos."),
|
|
153
|
+
attachments=[attachment_from_bytes(csv_bytes, "datos.csv", "text/csv")],
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Usar templates
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from mail_scheduler import TEMPLATE_REPORTE, MailTemplate
|
|
161
|
+
|
|
162
|
+
# Template predefinido
|
|
163
|
+
subject, body = TEMPLATE_REPORTE.render(
|
|
164
|
+
nombre="Gerente",
|
|
165
|
+
periodo="Abril 2026",
|
|
166
|
+
titulo="Ventas Q2",
|
|
167
|
+
resumen="Crecimiento del 12% respecto al mes anterior.",
|
|
168
|
+
)
|
|
169
|
+
await mail.send_mail(to=["gerente@outlook.com"], subject=subject, body=body)
|
|
170
|
+
|
|
171
|
+
# Template propio
|
|
172
|
+
aviso = MailTemplate(
|
|
173
|
+
name="aviso",
|
|
174
|
+
subject="Aviso: {{ titulo }}",
|
|
175
|
+
body="<p>Hola {{ nombre }},</p><p>{{ mensaje }}</p>",
|
|
176
|
+
)
|
|
177
|
+
subject, body = aviso.render(titulo="Cambio de horario", nombre="Equipo", mensaje="El lunes no hay clases.")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Guardar como borrador (para revisar antes de enviar)
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
draft = await mail.create_draft(
|
|
184
|
+
to=["cliente@outlook.com"],
|
|
185
|
+
subject="Propuesta comercial",
|
|
186
|
+
body=text_to_html("Estimado cliente,\n\nAdjunto nuestra propuesta."),
|
|
187
|
+
)
|
|
188
|
+
# Aparece en tu carpeta Drafts de Outlook para revisarlo antes de enviarlo
|
|
189
|
+
print(f"Borrador guardado: {draft.id}")
|
|
190
|
+
|
|
191
|
+
# Cuando estés listo:
|
|
192
|
+
await mail.send_draft(draft.id)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Scheduler con IA
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from mail_scheduler import OpenAIDraftClient, Scheduler, InMemoryStore, EmailBuilder
|
|
199
|
+
from datetime import datetime, timedelta, timezone
|
|
200
|
+
|
|
201
|
+
llm = OpenAIDraftClient(api_key=os.getenv("OPENAI_API_KEY"))
|
|
202
|
+
scheduler = Scheduler(mail_client=mail, llm_client=llm, store=InMemoryStore())
|
|
203
|
+
|
|
204
|
+
# Programa un correo: la IA genera el contenido justo antes de enviarlo
|
|
205
|
+
email = (
|
|
206
|
+
EmailBuilder()
|
|
207
|
+
.purpose("Recordar al equipo la entrega del proyecto del viernes")
|
|
208
|
+
.to(["equipo@outlook.com"])
|
|
209
|
+
.recipient_name("Equipo")
|
|
210
|
+
.tone("casual")
|
|
211
|
+
.language("es")
|
|
212
|
+
.send_at(datetime.now(timezone.utc) + timedelta(hours=2))
|
|
213
|
+
.build()
|
|
214
|
+
)
|
|
215
|
+
scheduler.schedule(email)
|
|
216
|
+
await scheduler.run_once()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Scheduler → borrador (revisar antes de enviar)
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
email = (
|
|
223
|
+
EmailBuilder()
|
|
224
|
+
.purpose("Responder al cliente sobre el retraso en la entrega")
|
|
225
|
+
.to(["cliente@outlook.com"])
|
|
226
|
+
.tone("formal")
|
|
227
|
+
.send_at(datetime.now(timezone.utc) + timedelta(minutes=5))
|
|
228
|
+
.save_as_draft() # la IA genera el contenido pero va a Drafts
|
|
229
|
+
.build()
|
|
230
|
+
)
|
|
231
|
+
scheduler.schedule(email)
|
|
232
|
+
await scheduler.run_once()
|
|
233
|
+
# Revisa tu carpeta Drafts, edita si necesitas, y envía desde Outlook
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Referencia rápida
|
|
239
|
+
|
|
240
|
+
| Función | Para qué |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `GraphAuthManager.login()` | Login OAuth con browser |
|
|
243
|
+
| `GraphAuthManager.refresh_access_token(token)` | Renovar token expirado |
|
|
244
|
+
| `GraphMailClient.get_me()` | Perfil del usuario autenticado |
|
|
245
|
+
| `GraphMailClient.list_messages(top, folder, only_unread)` | Listar mensajes |
|
|
246
|
+
| `GraphMailClient.get_message(id)` | Mensaje completo con body HTML |
|
|
247
|
+
| `GraphMailClient.send_mail(to, subject, body, ...)` | Enviar correo |
|
|
248
|
+
| `GraphMailClient.create_draft(to, subject, body, ...)` | Guardar borrador |
|
|
249
|
+
| `GraphMailClient.send_draft(draft_id)` | Enviar borrador existente |
|
|
250
|
+
| `GraphMailClient.reply_to_message(id, body)` | Responder manteniendo hilo |
|
|
251
|
+
| `GraphMailClient.mark_as_read(id)` | Marcar como leído |
|
|
252
|
+
| `GraphMailClient.move_to_trash(id)` | Mover a eliminados |
|
|
253
|
+
| `text_to_html(texto)` | Convertir texto plano a HTML |
|
|
254
|
+
| `attachment_from_bytes(data, filename)` | Adjunto desde memoria |
|
|
255
|
+
| `MailTemplate.render(**kwargs)` | Renderizar template |
|
|
256
|
+
| `EmailBuilder().purpose().to().send_at().build()` | Construir correo programado |
|
|
257
|
+
| `Scheduler.schedule(email)` | Agregar correo al scheduler |
|
|
258
|
+
| `Scheduler.run_once()` | Procesar correos vencidos |
|
|
259
|
+
| `Scheduler.run_loop(interval_seconds)` | Bucle continuo |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Estructura del proyecto
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
mail_scheduler/
|
|
267
|
+
├── authentication_microsoft/ OAuth2 + PKCE contra Microsoft
|
|
268
|
+
├── mail/ Cliente Graph + templates + adjuntos
|
|
269
|
+
├── LLM/ Generación de borradores con OpenAI
|
|
270
|
+
└── scheduler/ Programación y recurrencia de envíos
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Notas
|
|
276
|
+
|
|
277
|
+
- El `access_token` expira en **1 hora**. Usa `refresh_access_token()` para renovarlo sin pedir login de nuevo.
|
|
278
|
+
- El `refresh_token` expira en **90 días sin uso**. Si expira, el usuario debe hacer `login()` de nuevo.
|
|
279
|
+
- El `CLIENT_ID` no es un secreto — puedes compartirlo. Lo que nunca debes compartir son los tokens.
|
|
280
|
+
- Esta librería es solo para cuentas **personales** de Microsoft (`@outlook.com`, `@hotmail.com`). Para cuentas corporativas el proceso de registro es diferente.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from openai import AsyncOpenAI
|
|
4
|
+
|
|
5
|
+
from .prompts import build_draft_prompt
|
|
6
|
+
from .types import DraftResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenAIDraftClient:
|
|
10
|
+
"""
|
|
11
|
+
Small async client for generating email drafts with OpenAI.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, api_key: str | None = None, model: str = "gpt-5.4-mini"):
|
|
15
|
+
self._client = AsyncOpenAI(api_key=api_key)
|
|
16
|
+
self._model = model
|
|
17
|
+
|
|
18
|
+
async def test_connection(self, message: str) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Simple helper to verify that the LLM connection works.
|
|
21
|
+
"""
|
|
22
|
+
response = await self._client.responses.create(
|
|
23
|
+
model=self._model,
|
|
24
|
+
input=message,
|
|
25
|
+
)
|
|
26
|
+
return response.output_text.strip()
|
|
27
|
+
|
|
28
|
+
async def generate_draft(
|
|
29
|
+
self,
|
|
30
|
+
purpose: str,
|
|
31
|
+
recipient_name: str | None = None,
|
|
32
|
+
tone: str = "professional",
|
|
33
|
+
context: str | None = None,
|
|
34
|
+
language: str = "es",
|
|
35
|
+
) -> DraftResult:
|
|
36
|
+
"""
|
|
37
|
+
Generate an email draft and return structured subject/body output.
|
|
38
|
+
"""
|
|
39
|
+
prompt = build_draft_prompt(
|
|
40
|
+
purpose=purpose,
|
|
41
|
+
recipient_name=recipient_name,
|
|
42
|
+
tone=tone,
|
|
43
|
+
context=context,
|
|
44
|
+
language=language,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
response = await self._client.responses.create(
|
|
48
|
+
model=self._model,
|
|
49
|
+
input=prompt,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
text = response.output_text.strip()
|
|
53
|
+
subject, body = self._parse_subject_and_body(text)
|
|
54
|
+
|
|
55
|
+
return DraftResult(subject=subject, body=body, raw=text)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _parse_subject_and_body(text: str) -> tuple[str, str]:
|
|
59
|
+
"""
|
|
60
|
+
Parse a response of the form:
|
|
61
|
+
|
|
62
|
+
SUBJECT: ...
|
|
63
|
+
BODY: ...
|
|
64
|
+
"""
|
|
65
|
+
subject = ""
|
|
66
|
+
body = text
|
|
67
|
+
|
|
68
|
+
if "SUBJECT:" in text and "BODY:" in text:
|
|
69
|
+
before, after = text.split("BODY:", 1)
|
|
70
|
+
subject = before.replace("SUBJECT:", "").strip()
|
|
71
|
+
body = after.strip()
|
|
72
|
+
|
|
73
|
+
return subject, body
|