Configuración
Tres formas de construir un cliente, todas las opciones y lo que rechaza antes de enviar una solicitud.
Opciones
import os from openemail import AsyncOpenEmail, OpenEmail, init, openemail init(os.environ['OPENEMAIL_API_KEY'])openemail.me.ping() billing = OpenEmail(os.environ['BILLING_API_KEY']) pinned = OpenEmail('oe_live_...', base_url='https://api.openemail.uk') background = AsyncOpenEmail()| Punto de entrada | Qué te ofrece |
|---|---|
| init(...) | Configura el cliente compartido y lo devuelve. A partir de ese momento, openemail es ese cliente en todos los módulos, y todo lo que omitas se lee del entorno. |
| openemail | El cliente compartido. Si se usa antes de init, se construye a partir de OPENEMAIL_API_KEY y OPENEMAIL_BASE_URL en la primera llamada. |
| OpenEmail(...) | Un cliente independiente, para una segunda clave junto a la compartida, o para construir la instancia que exporta tu propio módulo. Todo lo que omitas se lee del entorno, igual que hace init, y create_client es la misma clase con otro nombre. |
| AsyncOpenEmail(...) | Un cliente asíncrono independiente, con las mismas opciones y con cada método llamado con await. El openemail compartido es síncrono, así que este lo construyes tú. |
| get_client() y reset_client() | get_client devuelve el propio cliente compartido y lo construye a partir del entorno si init no se ha ejecutado. reset_client lo olvida, así que el siguiente uso construye uno nuevo. |
import httpxfrom openemail import init init( 'oe_live_...', base_url='https://api.openemail.uk', timeout=30, max_retries=2, http_client=httpx.Client(proxy='http://proxy.internal:3128'), headers={'X-Team': 'billing'}, user_agent='billing-service/1.4', disable_update_notice=True,)| Opción | Valor por defecto | Notas |
|---|---|---|
| api_key | OPENEMAIL_API_KEY | init y OpenEmail la leen del entorno. Debe empezar por oe_live_ u oe_test_. |
| access_token | OPENEMAIL_ACCESS_TOKEN | Un token de acceso OAuth, o una función que devuelva uno, en lugar de api_key. Consulta Tokens de acceso OAuth más abajo. |
| base_url | https://api.openemail.uk | O OPENEMAIL_BASE_URL. La barra final se elimina, e init y OpenEmail anteponen https:// a un host sin esquema, o http:// a un host de esta máquina: localhost, una dirección 127.x.x.x o ::1. Nunca se envía una credencial por http sin cifrar a ningún otro host, y 0.0.0.0 o [::] lanza una excepción al crear el cliente, porque son direcciones en las que escucha un servidor, no a las que enviar solicitudes. |
| timeout | 30 | En segundos, por intento, no por llamada. Cubre la lectura del cuerpo, no solo de las cabeceras. 0 lo desactiva. files.upload permite al menos 600 segundos salvo que la llamada pase su propio timeout. |
| max_retries | 2 | Intentos adicionales después del primero, en llamadas que se pueden repetir sin riesgo. Se configura en el cliente, no por llamada. |
| http_client | un httpx.Client nuevo | Pasa el tuyo para un proxy, tu propia configuración de TLS, un transporte montado o un doble de prueba: un httpx.Client a OpenEmail y un httpx.AsyncClient a AsyncOpenEmail. Cerrar el cliente deja abierto el que pasaste. |
| headers | {} | Se envían en cada solicitud. |
| user_agent | openemail-python/<version> | Se envían en cada solicitud. |
| disable_update_notice | False | Omite la comprobación, una vez por proceso, de si hay una versión más reciente en PyPI. La comprobación solo se ejecuta cuando la salida va a una terminal, y OPENEMAIL_DISABLE_UPDATE_NOTICE también la desactiva. |
Lo que rechaza antes de enviar
Estos casos lanzan ValueError, o TypeError donde lo indica la tabla, antes de enviar ninguna solicitud, en lugar de aparecer como un fallo confuso en tu primer envío. El mensaje indica qué estaba mal y qué pasar en su lugar.
| Rechazado | Por qué |
|---|---|
| Ninguna clave | No se definió ni api_key ni OPENEMAIL_API_KEY, así que no hay nada con lo que autenticarse. |
| Una cookie de sesión o un token de sesión | Aquí solo autentican oe_live_ y oe_test_, y la API lo confirma. La comprobación es un prefijo y nada más, así que una clave revocada sigue fallando en la red. |
| Un base_url que no es una URL http o https | No se puede obtener nada más, así que el cliente lo rechaza al construirse en lugar de fallar en la primera solicitud. |
| Un base_url en 0.0.0.0 o [::] | Es una dirección en la que escucha un servidor, no una a la que enviar solicitudes. En su lugar, el mensaje propone 127.0.0.1 o [::1] con el mismo puerto. |
| Una credencial por http sin cifrar | Se rechaza en la llamada, antes de que salga la solicitud, salvo que el servidor esté en esta máquina. Cualquiera en la red podría leerla. |
| Un id vacío o compuesto solo de puntos en cualquier método | Se lanza al llamar al método. Todos los analizadores de URL eliminan un segmento de ruta formado por puntos, así que la solicitud llegaría a otro endpoint. |
| Un http_client del tipo equivocado | Un TypeError al construir el cliente. OpenEmail acepta un httpx.Client, y AsyncOpenEmail, un httpx.AsyncClient. |
| Un valor del cuerpo que JSON no puede representar | Un TypeError que nombra su tipo. Los tipos de JSON pasan tal cual, y un datetime, un date o un set se convierten automáticamente. |
No existe una opción test_mode ni la habrá. El esquema de la clave forma parte de la credencial en lugar de ser una pista, así que el modo es una propiedad de la clave. client.mode lee el prefijo y no decide nada.
Un cliente, varias claves
Construye el cliente una vez y compártelo. Una instancia nueva por solicitud desecha el pool de conexiones y la configuración sin ganar nada, y ninguno de sus estados es propio de cada llamante.
Un mismo cliente se puede compartir entre hilos sin riesgo. close() o el final de un bloque with cierra el pool de conexiones que abrió, y un http_client que hayas pasado sigue abierto para que lo cierres tú.
Para el caso que de otro modo obligaría a una instancia por clave, como un trabajo que envía en nombre de varios espacios de trabajo, pasa api_key en la llamada. Sustituye la cabecera Authorization de esa solicitud y no deja nada en el cliente.
from openemail.types import EmailSend workspace_key = 'oe_live_...' message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} client.emails.send(message) client.emails.send(message, api_key=workspace_key) client.threads.list(folder='inbox', api_key=workspace_key)client.webhooks.list(api_key=workspace_key)Todos los métodos fuera de temp_mail la aceptan como argumento nombrado, junto a timeout. Se comprueba antes de enviar la solicitud, con la misma regla que usa el constructor, así que una errata lanza un ValueError que menciona api_key= on this call en lugar de un 401 sobre una credencial que luego tienes que ir a buscar. Una llamada reintentada conserva la clave que se le dio.
timeout va en segundos y sustituye el tiempo de espera del cliente en esa única llamada, en cada uno de sus intentos.
client.mode describe la clave con la que se CONSTRUYÓ el cliente y no sigue a una sustitución puntual. En cuanto un cliente sirve a varias claves no hay un único modo que informar, así que dedúcelo de la clave que pasaste.
Un endpoint que ningún método envuelve
client.raw.request envía una solicitud aplicando la credencial, la URL base, el tiempo de espera y la política de reintentos del cliente, y devuelve el JSON analizado. Acepta method, query, body, api_key y timeout. Reintenta un GET y envía cualquier otra cosa una sola vez, salvo que pases repeatable=True. idempotent=True añade un Idempotency-Key, el que pases como idempotency_key o uno nuevo.
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})La ruta debe empezar por una sola /. Cualquier otra cosa, como //host/x, lanza una excepción antes de enviar la solicitud, y lo mismo ocurre con una ruta cuya URL final sale del origen de la URL base, así que la credencial que lleva nunca llega a otro host.
Bandejas desechables
create_temp_mail() construye un cliente que no lleva ninguna clave de API, y create_async_temp_mail() es su equivalente asíncrono. Crea buzones de forma anónima, y cada lectura envía el token de buzón que devolvió create, o el más reciente que devolvió extend, bien por llamada como inbox_token, bien una sola vez como create_temp_mail(inbox_token=...).
from openemail import create_temp_mail temp_mail = create_temp_mail() inbox = temp_mail.create()messages = temp_mail.list_messages(inbox['id'], inbox_token=inbox['token']) print(messages['items'], messages['expiresAt'])Tokens de acceso OAuth
Aún no disponibleUna app que una persona conectó por OAuth, como una herramienta de línea de comandos o un agente, tiene un token de acceso en lugar de una clave de API. Pásalo como access_token, ya sea el propio token o una función que lo devuelva, que en AsyncOpenEmail puede ser async. La función se llama una vez por cada llamada, y los reintentos de esa llamada reutilizan lo que devolvió, así que renueva el token dentro de ella cuando esté a punto de caducar y nunca tendrás que reconstruir el cliente.
from openemail import OpenEmail client = OpenEmail(access_token=session.fresh_access_token) me = client.me.get() if me['object'] == 'oauth_token': print(me['clientId'], me['expiresAt'])| Caso | Qué ocurre |
|---|---|
| api_key y access_token a la vez, o ninguno | El constructor lanza ValueError. Si no hay ninguno, el mensaje nombra OPENEMAIL_API_KEY y OPENEMAIL_ACCESS_TOKEN. |
| Un valor que no es un token | Un token tiene de 1 a 512 caracteres y no empieza por oe_, la comprobación que hace is_access_token. Una cadena que no la supera lanza una excepción desde el constructor, y una función que devuelve un valor así hace fallar la llamada antes de enviar nada. |
| OPENEMAIL_ACCESS_TOKEN | Lo leen init, OpenEmail y el openemail compartido cuando no pasas ninguna de las dos credenciales y OPENEMAIL_API_KEY no está definida, así que una clave en el entorno tiene prioridad. |
| Una función que lanza una excepción | La llamada lanza ese mismo error, sin cambios, y no se envía nada. |
| Una función en OpenEmail que devuelve un objeto awaitable | Un ValueError, porque el cliente síncrono no puede esperarlo. En AsyncOpenEmail la función puede ser async. |
| Un api_key por llamada | Sustituye el token para esa única solicitud, y la función no se llama. |
| mode | Siempre live con un token. |
| create_temp_mail() | No envía ninguna credencial, contenga lo que contenga el entorno. |
| me.get() y me.ping() | Para un token, get responde con object igual a oauth_token, id y roleId a None, el clientId de la app conectada y expiresAt, el momento en que caduca la aprobación que la persona dio a la app. ping responde con kind igual a oauth, keyId a None y el clientId. KeyResource y PingResource son uniones, así que comprueba object o kind antes de leer clientId o expiresAt. |
Un token actúa en nombre de una persona y lee su correo como ella puede, así que guárdalo en un servidor igual que una clave.
Códigos de verificación
Aún no disponibleAntes de un cambio delicado, como eliminar un dominio o cambiar un webhook, la API pide a un token de acceso el código de verificación que la app web le pediría a la persona. La llamada lanza un OpenEmailApiError cuyo is_step_up_required es True, y no se cambió nada. Pide un código, verifica el que te dé la persona y vuelve a hacer la llamada. A una clave de API nunca se le pide.
from openemail import OpenEmailApiError try: client.domains.delete(domain_id)except OpenEmailApiError as error: if not error.is_step_up_required: raise challenge = client.security.begin_step_up() if challenge['method'] == 'email': prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: ' else: prompt = 'Enter the code from your authenticator app, or a backup code: ' client.security.verify_step_up({'code': input(prompt)}) client.domains.delete(domain_id)| Método | Qué hace |
|---|---|
| security.step_up_status() | Si la app está verificada ahora mismo (elevated, elevatedUntil), cómo se comprueba el siguiente código (method, email o totp) y minutes, la duración de la ventana. No envía nada ni informa de una pausa. |
| security.begin_step_up(body=None) | Abre un desafío. Con email sale un código de seis dígitos hacia la dirección con la que la persona inicia sesión, y sentTo la muestra enmascarada. Con totp la persona lee uno en su aplicación de autenticación o usa un código de recuperación. Un desafío que sigue abierto y aún tiene intentos se reutiliza salvo que pases {'resend': True}, y uno bloqueado o caducado se sustituye con una llamada simple. Cada app puede abrir 5 por hora y 20 en 24 horas por persona, y la siguiente lanza un 429 step_up_throttled. |
| security.verify_step_up({'code': code}) | Comprueba el código y desbloquea los cambios delicados para esta app durante 60 minutos, hasta elevatedUntil, por REST y a través de las herramientas MCP que hacen los mismos cambios. Tras 10 códigos incorrectos en 24 horas de esta app, o 20 de todas las apps de la persona juntas, esta llamada y begin_step_up lanzan un 429 step_up_locked con un mensaje que dice cuándo se reanuda la verificación. |
El cliente nunca pide un código ni repite la llamada por su cuenta, y ni begin_step_up ni verify_step_up se reintentan automáticamente, porque un reintento tras una respuesta perdida podría enviar un segundo correo o gastar un segundo intento. No necesitan ningún ámbito, y una clave de API que llame a uno recibe 400 step_up_not_applicable. STEP_UP_ERROR_CODES nombra todas las formas en que puede fallar una verificación, y la página de errores de la API dice qué hacer con cada una.