Async email scheduling, drafting, and Microsoft Graph mail integration.
Project description
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.
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.
- Ve a entra.microsoft.com e inicia sesión con tu cuenta personal
- Ve a Applications → App registrations → New registration
- 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
- Name: el nombre que quieras (ej.
- Haz clic en Register
- 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_secretque no necesitas con esta librería.
3. Configurar permisos
En tu app registration recién creada:
- Ve a API Permissions → Add a permission → Microsoft Graph → Delegated
- 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 |
- Haz clic en Add permissions
No necesitas client_secret — la librería usa PKCE, el estándar para aplicaciones públicas.
Instalación
pip install -r requirements.txt
Configuración
Crea un archivo .env en tu proyecto (nunca lo subas a git):
MAILSCHEDULER_CLIENT_ID=tu-application-client-id
MAILSCHEDULER_TENANT=consumers
OPENAI_API_KEY=api-key
Uso básico
Login
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
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
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
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)
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
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)
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_tokenexpira en 1 hora. Usarefresh_access_token()para renovarlo sin pedir login de nuevo. - El
refresh_tokenexpira en 90 días sin uso. Si expira, el usuario debe hacerlogin()de nuevo. - El
CLIENT_IDno 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:
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file yurmail-0.2.0.tar.gz.
File metadata
- Download URL: yurmail-0.2.0.tar.gz
- Upload date:
- Size: 35.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d36c5b285d2da52e8a18c49afaa7500e098f244749dd2f9e0cedb05d35d802f3
|
|
| MD5 |
de857f68e284cbe9a68b1111f848a240
|
|
| BLAKE2b-256 |
cc385cecbac25ed15a3056d711d49092b7db6837722ce89939a0e20c86185971
|
File details
Details for the file yurmail-0.2.0-py3-none-any.whl.
File metadata
- Download URL: yurmail-0.2.0-py3-none-any.whl
- Upload date:
- Size: 29.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e0bed5984c33a0eae62fc7e781bb9807ca538904f202f7b3dbc4d6390d8001a
|
|
| MD5 |
44609035b6b9715e5fb29f717c7267dd
|
|
| BLAKE2b-256 |
9b0ba9cff92c4d2b9f6b0ba6be0755a295008bea4b932cfa8c86e89c19400a45
|