En una prueba de registro o recuperación de contraseña, crear un email temporal suele parecer la parte fácil. El problema aparece después: el buzón queda vivo, otro test lo reutiliza y nadie sabe cuándo se puede borrar. En CI, esa pequeña ambigüedad termina en pruebas intermitentes y datos que se acumulan.

Una solución práctica es tratar cada buzón de prueba como un recurso con contrato de expiración. No hace falta construir una plataforma enorme. Con FastAPI, un identificador de ejecución, una fecha límite y una tarea de limpieza se puede hacer el comportamiento mucho más predecible.

El problema: un buzón de prueba que nunca muere

Un flujo típico hace esto:

  1. Crea una dirección temporal.
  2. La entrega al navegador o al cliente API.
  3. Espera el mensaje de confirmación.
  4. Comprueba el enlace.
  5. Abandona el recurso.

El quinto paso es el que suele faltar. Si el test pasa, nadie se acuerda del buzón. Si falla, el diagnóstico puede dejar varios recursos abiertos. Y si dos ejecuciones usan el mismo identificador, una limpieza puede borrar los datos que todavía necesita la otra.

También aparecen entradas poco útiles en los logs cuando alguien escribe tempail mail o tepm mail com y termina probando otra configuración. Parece pequeño, pero después cuesta saber qué dato pertenece a cada corrida.

Conviene separar tres conceptos: propietario, caducidad y estado. El propietario identifica la ejecución que creó el recurso; la caducidad dice hasta cuándo es válido; el estado distingue entre activo, consumido y expirado. Esta separación también ayuda a aislar las pruebas de email por entorno.

Qué debe prometer el contrato

Antes de escribir endpoints, define promesas pequeñas y verificables:

  • Cada recurso pertenece a un run_id único.
  • La fecha expires_at se calcula en el servidor, no en el navegador.
  • Leer mensajes después de expirar devuelve un error claro.
  • Repetir la operación de expiración no rompe nada.
  • La limpieza no toca recursos de otra ejecución.

No es un sistema de correo de producción. Es una frontera de datos para testing. Por eso el contrato debe ser sencillo, observable y fácil de borrar. Para flujos más automáticos, también merece la pena pensar en límites seguros para automatizar email.

Los nombres de estado importa más de lo que parecen: si son vagos, cada cliente interpreta el ciclo de vida a su manera. Al principio parece un poco más trabajo, pero ahorra conversaciones raras en el equipo.

Un modelo pequeño en FastAPI

Este ejemplo usa memoria para mostrar la idea. En un servicio real, la misma información iría a PostgreSQL o Redis, con un índice por run_id y expires_at.

from datetime import datetime, timedelta, timezone from uuid import uuid4 from fastapi import FastAPI, HTTPException app = FastAPI() mailboxes: dict[str, dict] = {} TTL_MINUTES = 15 def now() -> datetime: return datetime.now(timezone.utc) @app.post("/test-mailboxes") def create_mailbox(run_id: str): mailbox_id = str(uuid4()) expires_at = now() + timedelta(minutes=TTL_MINUTES) mailboxes[mailbox_id] = { "run_id": run_id, "status": "active", "expires_at": expires_at, "messages": [], } return {"id": mailbox_id, "expires_at": expires_at} @app.get("/test-mailboxes/{mailbox_id}/messages") def list_messages(mailbox_id: str): mailbox = mailboxes.get(mailbox_id) if mailbox is None: raise HTTPException(status_code=404, detail="mailbox not found") if mailbox["expires_at"] <= now(): mailbox["status"] = "expired" raise HTTPException(status_code=410, detail="mailbox expired") return {"messages": mailbox["messages"]} 

El código usa UTC y responde 410 Gone cuando el recurso ya no debe utilizarse. Ese detalle es más útil que devolver una lista vacía: el test sabe si debe reintentar la espera o crear un buzón nuevo. Un detalle que se olvida facil es no mezclar datetime sin zona horaria con valores UTC; tarde o temprano produce comparaciones raras.

Expirar sin carreras ni borrados peligrosos

La expiración no debería depender de que un test llame a un endpoint final. Añade un trabajo periódico que busque recursos vencidos y los marque o borre. Pero hazlo con una condición que incluya la fecha límite:

DELETE FROM test_mailboxes WHERE expires_at <= CURRENT_TIMESTAMP AND status IN ('active', 'consumed'); 

Si una ejecución todavía está leyendo un mensaje, puedes preferir una fase de gracia: primero marcas expired, esperas unos segundos y después borras. En Redis, un TTL puede servir para el almacenamiento, pero conserva también run_id en los datos para poder auditar qué pasó.

La limpieza ocurren después de la expiración, no antes de que venza el plazo. Esa diferencia evita que un job lento pierda su evidencia justo cuando más la necesita.

La operación debe ser idempotente. Ejecutar el limpiador una vez o cinco veces debe dejar el mismo resultado. También conviene registrar mailbox_id, run_id, motivo y duración de vida; esos cuatro campos dan una pista bastante buena cuando el CI falla de noche.

Evita renovar el TTL en cada lectura. Si el test está bloqueado esperando un mensaje, podría mantener el recurso vivo para siempre. El contrato debe tener una fecha máxima que solo un endpoint explícito y autorizado pueda extender, si de verdad hace falta.

Si el consumidor se queda esperando por mucho tiempo, devuelve un estado intermedio y registra el intento; no lo dejes esperando para siempre. Es una regla sencilla, pero suele olvidarse facil cuando el flujo feliz funciona.

Q&A rápido

¿Por qué no devolver siempre una lista vacía?

Porque una lista vacía puede significar “el email todavía no llegó” o “el buzón ya no existe”. Diferenciar 200, 404 y 410 hace que el cliente tome decisiones más sanas.

¿Es suficiente un run_id?

Es una buena base, pero debe ser impredecible entre ejecuciones y no contener datos personales. Añade un índice y comprueba la propiedad antes de leer o borrar.

¿Qué TTL conviene usar?

Empieza con el tiempo máximo real de la prueba más un margen pequeño, por ejemplo 15 minutos para un flujo que normalmente tarda dos. Mídelo después: un TTL gigante solo es una forma lenta de esconder fugas.

Checklist para llevarlo a CI

  • Genera un run_id nuevo por job y por intento.
  • Guarda expires_at en UTC y devuelve la fecha al cliente.
  • Devuelve 410 para un buzón vencido.
  • Ejecuta una limpieza periódica y otra al terminar el job.
  • No reutilices direcciones entre pruebas paralelas.
  • Prueba la doble expiración y la lectura durante la limpieza.
  • Conserva métricas de recursos activos, vencidos y borrados.

El objetivo no es simular cada detalle de un proveedor de correo. Es hacer explícito el ciclo de vida del dato temporal. Cuando FastAPI expone esa frontera con estados claros, los tests dejan de depender de suerte y la automatización se vuelve un poco más tranquila, incluso cuando algo falla.