Ir a la documentación
Ruby

Configuración

Tres formas de construir un cliente, todas las opciones y lo que rechaza antes de enviar una solicitud.

Opciones

clients.rb
require "openemail" OpenEmail.init(api_key: ENV.fetch("OPENEMAIL_API_KEY"))OpenEmail.me.ping pinned = OpenEmail::Client.new(api_key: ENV.fetch("OPENEMAIL_API_KEY"), base_url: "https://api.openemail.uk")quick = OpenEmail::Client.new(ENV.fetch("OPENEMAIL_API_KEY"))billing = OpenEmail.create_client(api_key: ENV.fetch("BILLING_API_KEY")) p pinned.mode, quick.mode, billing.mode
Punto de entradaQué te ofrece
OpenEmail.init(...)Configura el cliente compartido y lo devuelve. A partir de ese momento, OpenEmail.client es ese cliente en todos los archivos y todos los hilos de ejecución, y todo lo que omitas se lee del entorno.
OpenEmail.client, OpenEmail.emails, OpenEmail.threads y todos los demás espacios de nombresEl cliente compartido y atajos a sus espacios de nombres. Si se usa antes de init, se construye en la primera llamada a partir de OPENEMAIL_API_KEY y OPENEMAIL_BASE_URL.
OpenEmail.reset_clientDescarta el cliente compartido, de modo que la siguiente llamada construye uno nuevo a partir del entorno.
OpenEmail.create_client(...)Un cliente independiente que también recurre al entorno, para una segunda clave junto a la compartida, o para un cliente que tu propio código guarda y pasa de un sitio a otro.
OpenEmail::Client.new(...) or OpenEmail::Client.new(api_key)Un cliente independiente construido exactamente con lo que le pasas. No lee el entorno, así que necesita api_key: o access_token:. OpenEmail.new es la misma llamada.
options.rb
OpenEmail.init(  api_key: ENV.fetch("OPENEMAIL_API_KEY"),  base_url: "https://api.openemail.uk",  timeout: 30,  max_retries: 2,  adapter: OpenEmail::NetHttpAdapter.new(max_idle: 8, keep_alive_timeout: 2),  headers: {"X-Team" => "billing"},  user_agent: "billing-service/1.4",  disable_update_notice: true)
OpciónValor por defectoNotas
api_key:OPENEMAIL_API_KEYinit, create_client y el cliente compartido la leen del entorno. Debe empezar por oe_live_ u oe_test_. También puede ser el primer argumento, pero no ambas cosas.
access_token:OPENEMAIL_ACCESS_TOKENUn token de acceso OAuth, o cualquier objeto que responda a call y devuelva uno. Consulta Tokens de acceso OAuth más abajo. Pasa una clave o un token, nunca ambos.
base_url:https://api.openemail.ukO OPENEMAIL_BASE_URL. Las barras finales se eliminan, e init y create_client 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 un error al construir el cliente, porque son direcciones en las que escucha un servidor, no a las que enviar solicitudes.
timeout:30Segundos por intento, no por llamada. Con el adaptador por defecto cubre la conexión y la lectura del cuerpo entero, no solo de las cabeceras. 0 lo desactiva. files.upload espera al menos 600 segundos salvo que pases timeout: en esa llamada.
max_retries:2Intentos adicionales después del primero, en llamadas que se pueden repetir sin riesgo. Se configura en el cliente, no por llamada. 0 desactiva los reintentos.
adapter:OpenEmail::NetHttpAdapter.newLa capa HTTP. La predeterminada mantiene hasta 8 conexiones inactivas por host durante 2 segundos cada una, y max_idle: y keep_alive_timeout: lo cambian. Cualquier objeto que responda a call(request) puede ocupar su lugar, y así es como una prueba se ejecuta sin red.
headers:{}Se envían en cada solicitud.
user_agent:openemail-ruby/<version>Se envían en cada solicitud.
disable_update_notice:falseOmite la comprobación, una vez por proceso, de si hay una versión más reciente en RubyGems. La comprobación solo se ejecuta cuando la salida estándar es una terminal, y OPENEMAIL_DISABLE_UPDATE_NOTICE también la desactiva.

Variables de entorno

VariableQué hace
OPENEMAIL_API_KEYLa clave que usan init, create_client y el cliente compartido cuando no pasas ni api_key: ni access_token:.
OPENEMAIL_ACCESS_TOKENUn token de acceso OAuth, que solo se lee cuando no pasas ninguna de las dos credenciales y OPENEMAIL_API_KEY no está definida, así que una clave en el entorno tiene prioridad.
OPENEMAIL_BASE_URLLa URL base cuando no pasas ninguna. A un host sin esquema, como localhost:2222, se le añade el esquema.
OPENEMAIL_DISABLE_UPDATE_NOTICECualquier valor no vacío desactiva el aviso de actualización para todos los clientes del proceso.
HTTPS_PROXY y NO_PROXY, o https_proxy y no_proxyEl proxy a través del cual se conecta el adaptador por defecto, y los hosts a los que se va directamente. Consulta Proxies más abajo.

OpenEmail::Client.new no lee ninguna de las tres primeras, así que un cliente construido así nunca toma por accidente una clave del entorno. Una variable definida pero vacía cuenta como no definida.

Lo que rechaza antes de enviar

Estos casos lanzan ArgumentError desde la línea que contenía el valor incorrecto, 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, y nunca repite una credencial.

RechazadoPor qué
Ninguna credencialNo se pasó ni api_key: ni access_token:, y para init y create_client tampoco estaba definida ninguna de las dos variables, así que no hay nada con lo que autenticarse. Se lanza al construir el cliente.
Una clave y un token a la vezCada solicitud lleva una sola credencial, así que el cliente no puede saber a cuál te referías. Una clave pasada a la vez como primer argumento y como api_key: se rechaza por el mismo motivo.
Una cookie de sesión, un token de sesión o una clave de otro servicioAquí 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, como OpenEmail::AuthenticationError.
Un base_url: que no es una URL http o https, o que contiene un nombre de usuario o una contraseñaNo se puede llegar a nada más, y una credencial va en api_key: o access_token:, no en la URL. Se lanza al construir el cliente.
Una credencial por http sin cifrar hacia un host que no está en esta máquinaLo lanza la llamada, antes de enviar nada. Usa una URL base https.
Un timeout: que no es un número de segundos, o que es negativoPasa segundos, o 0 para no tener tiempo de espera. Se lanza al construir el cliente.
Un nombre de cabecera que no es un token, o un salto de línea en el valor de una cabeceraSe comprueba en headers:, user_agent: e idempotency_key:, porque un salto de línea iniciaría una segunda cabecera.
Un id vacío o compuesto solo de puntos en cualquier métodoSe 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. También se rechaza un id que no sea UTF-8 válido.
Un cuerpo de solicitud que no es un HashPasa argumentos nombrados o un solo Hash. Cualquier objeto que responda a to_hash cuenta como tal.

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, "live" o "test", y no decide nada.

Un cliente, varias claves

Construye el cliente una vez y compártelo. Un cliente nuevo por solicitud desecha sus conexiones abiertas sin ganar nada, y ninguno de sus estados es propio de cada llamante. Un cliente queda congelado una vez construido y se puede usar sin riesgo desde muchos hilos de ejecución a la vez, así que un proceso de Puma o Sidekiq solo necesita uno, y tras un fork el proceso hijo abre sus propias conexiones.

Para el caso que de otro modo obligaría a un cliente 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.

per_call_key.rb
message = {from: "[email protected]", to: "[email protected]", subject: "Your invoice", text: "Attached."}workspace_key = ENV.fetch("OPENEMAIL_API_KEY") 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 los filtros en una lista, y los métodos de temp_mail aceptan inbox_token: en su lugar. Se comprueba antes de enviar la solicitud, con la misma regla que usa el cliente, así que una errata lanza un ArgumentError sobre la api_key pasada a esta llamada en lugar de un 401 sobre una credencial que luego tienes que ir a buscar. Una llamada reintentada conserva la clave que recibió.

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. client.inspect muestra el modo y la URL base, nunca la clave.

Endpoints que ningún método envuelve

client.raw es el transporte por el que pasa cada método. client.raw.request llama a una ruta que ningún método envuelve todavía, aplicando la credencial, la URL base, el tiempo de espera y la política de reintentos del cliente, y devuelve el cuerpo analizado igual que un método.

raw_request.rb
ping = client.raw.request("/ping") label = client.raw.request("/labels", method: :post, body: {name: "Invoices"}) p ping[:ok], label[:id]
ArgumentoQué hace
method::get salvo que indiques otro: :post, :put, :patch o :delete.
query:Un Hash de parámetros de consulta. Los valores nil y vacíos se omiten, un Array o un Set se une con comas, y un Time se envía como un instante ISO 8601.
body:Un Hash, enviado como JSON.
raw: y content_type:Bytes que se envían tal cual, como String binaria, IO o Pathname, con application/octet-stream salvo que indiques un tipo.
accept: y binary:Un accept: distinto de JSON devuelve el cuerpo como texto, y binary: true lo devuelve como String binaria.
idempotent: e idempotency_key:idempotent: true adjunta un Idempotency-Key, que se genera salvo que pases el tuyo.
repeatable:Si un fallo se reintenta. Solo se reintenta un GET, salvo que pases repeatable: true.
api_key: y timeout:La misma clave por llamada, y un tiempo de espera en segundos solo para esta llamada.

La ruta debe empezar por una sola /, y una ruta cuya URL final saldría del origen de la URL base lanza ArgumentError antes de enviar nada, así que la credencial nunca llega a otro host.

Bandejas desechables

OpenEmail.create_temp_mail construye un cliente para buzones desechables que no lleva ninguna clave de API ni lee ninguna del entorno. 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 OpenEmail.create_temp_mail(inbox_token:).

temp_mail.rb
temp_mail = OpenEmail.create_temp_mail inbox = temp_mail.createpage = temp_mail.list_messages(inbox[:id], inbox_token: inbox[:token]) p page.items.size, page.expires_at

create_temp_mail acepta base_url:, adapter:, max_retries:, timeout:, user_agent:, headers: y disable_update_notice: como cualquier cliente, y lee OPENEMAIL_BASE_URL cuando no pasas ninguna URL base.

Tokens de acceso OAuth

Una 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 cualquier objeto que responda a call y lo devuelva, como una lambda o un Method. Se invoca una vez por cada llamada, y los reintentos de esa llamada reutilizan lo que devolvió, así que renueva el token dentro de él cuando esté a punto de caducar y nunca tendrás que reconstruir el cliente.

access_token.rb
tokens = {current: "token-from-your-oauth-flow"} oauth_client = OpenEmail::Client.new(access_token: -> { tokens.fetch(:current) }) me = oauth_client.me.get puts me[:clientId], me[:expiresAt] if me[:object] == "oauth_token"
CasoQué ocurre
api_key: y access_token: a la vez, o ningunoEl cliente lanza ArgumentError al construirse. Si no hay ninguno, el mensaje nombra OPENEMAIL_API_KEY y OPENEMAIL_ACCESS_TOKEN.
Un valor que no es un tokenUn token tiene de 1 a 512 caracteres y no empieza por oe_, la comprobación que hace OpenEmail.access_token?. Una String que no la supera lanza un error al construir el cliente, y un invocable que devuelve una así lanza ArgumentError desde la llamada antes de enviar nada.
OPENEMAIL_ACCESS_TOKENLo leen init, create_client y el cliente 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.
Un invocable que lanza un errorLa llamada lanza ese mismo error, sin cambios, y no se envía nada.
Un api_key: por llamadaSustituye el token para esa única solicitud, y el invocable no se invoca.
client.modeSiempre "live" con un token.
OpenEmail.create_temp_mailNo envía ninguna credencial, contenga lo que contenga el entorno.
me.get y me.pingPara un token, get responde con object igual a oauth_token, id y roleId a nil, 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 nil y el clientId. Comprueba object o kind antes de leer id o keyId.

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

Antes 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 OpenEmail::PermissionError, un 403 cuyo 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.

step_up.rb
domain_id = "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f" begin  client.domains.delete(domain_id)rescue OpenEmail::ApiError => error  raise unless error.step_up_required?   challenge = client.security.begin_step_up   if challenge[:method] == "email"    puts "Enter the code we emailed to #{challenge[:sentTo]}"  else    puts "Enter the code from your authenticator app, or a backup code"  end   client.security.verify_step_up(code: $stdin.gets.to_s.strip)  client.domains.delete(domain_id)end
MétodoQué hace
security.step_up_statusSi 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_upAbre 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 para cada persona, y el siguiente lanza un 429 step_up_throttled.
security.verify_step_up(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 ninguno de los tres métodos se reintenta 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 un 400 step_up_not_applicable. OpenEmail::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 en cada caso.

El aviso de actualización

Cuando hay una versión más reciente de la gema en RubyGems, el cliente lo dice una vez por proceso, en la salida de error estándar, con una línea como ℹ openemail 0.0.2 is available, you are on 0.0.1. seguida de la página de la gema. La comprobación se ejecuta al construir el primer cliente, en un hilo en segundo plano con un tiempo de espera de dos segundos, solo cuando la salida estándar es una terminal, y si no se puede llegar a RubyGems, se ignora.

La comprobación pasa por el adaptador del cliente, así que un adaptador de pruebas puede ver una solicitud a RubyGems cuando las pruebas se ejecutan en una terminal. Construye los clientes de prueba con disable_update_notice: true, o define OPENEMAIL_DISABLE_UPDATE_NOTICE.

Proxies

El adaptador por defecto encuentra su proxy con el propio URI#find_proxy de Ruby, así que sigue las mismas reglas que el resto de la biblioteca estándar: https_proxy o HTTPS_PROXY nombra el proxy, y no_proxy o NO_PROXY enumera los hosts a los que se va directamente. Un nombre de usuario y una contraseña en la URL del proxy se envían al proxy, y a un servidor de esta máquina nunca se llega a través de uno.

Las conexiones usan TLS 1.2 o posterior y comprueban el certificado del servidor, así que un proxy que inspecciona TLS necesita que OpenSSL confíe en su autoridad de certificación en esa máquina.

Pruebas sin red

adapter: sustituye la capa HTTP. Es cualquier objeto que responda a call(request), una lambda incluida, y devuelva un OpenEmail::HttpResponse con status, headers y body. La solicitud es un OpenEmail::HttpRequest con method, url, headers, body y timeout, así que una prueba puede comprobar exactamente lo que habría salido.

fake_adapter.rb
requests = [] adapter = lambda do |request|  requests << request  OpenEmail::HttpResponse.new(    status: 200,    headers: {"content-type" => "application/json"},    body: JSON.generate({id: "msg_test", status: "sent", replayed: false})  )end test_client = OpenEmail::Client.new(api_key: "oe_test_fake", adapter:, max_retries: 0, disable_update_notice: true) sent = test_client.emails.send(from: "[email protected]", to: "[email protected]", subject: "Hi", text: "Hello") p sent[:status], requests.first.method, requests.first.url, requests.first.headers["Idempotency-Key"]p requests.first

Al imprimir una solicitud, su cabecera Authorization aparece como [redacted], así que un registro de pruebas nunca contiene la clave.

  • Devuelve un status fuera de 2xx con el sobre de error de la API como cuerpo, por ejemplo {"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}, para obtener la subclase de OpenEmail::ApiError correspondiente.
  • Lanza Timeout::Error desde call, o Net::ReadTimeout, que es uno de ellos, para obtener un OpenEmail::NetworkError cuyo timeout? es true. Cualquier otro StandardError, como Errno::ECONNREFUSED, se convierte en un NetworkError cuyo timeout? es false.
  • NameError, TypeError y ArgumentError lanzados dentro del adaptador cuentan como errores de programación suyos. Se lanzan sin cambios y nunca se reintentan.

Construye un cliente de prueba con max_retries: 0 cuando simules fallos. Si no, un status reintentable o un fallo de red en una llamada que se puede repetir sin riesgo se intenta tres veces, con esperas reales entre medias.