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 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.
@@ -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,4 @@
1
+ from .client import OpenAIDraftClient
2
+ from .types import DraftResult
3
+
4
+ __all__ = ["OpenAIDraftClient", "DraftResult"]
@@ -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