Configuração
Três formas de construir um cliente, todas as opções e o que ele recusa antes de um pedido ser enviado.
Opções
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| Ponto de entrada | O que obtém |
|---|---|
| OpenEmail.init(...) | Configura o cliente partilhado e devolve-o. A partir daí, OpenEmail.client é esse cliente em todos os ficheiros e em todas as threads, e tudo o que omitir é lido do ambiente. |
| OpenEmail.client, OpenEmail.emails, OpenEmail.threads e todos os outros espaços de nomes | O cliente partilhado e atalhos para os seus espaços de nomes. Se for usado antes de init, constrói-se a si próprio na primeira chamada a partir de OPENEMAIL_API_KEY e OPENEMAIL_BASE_URL. |
| OpenEmail.reset_client | Descarta o cliente partilhado, para que a chamada seguinte construa um novo a partir do ambiente. |
| OpenEmail.create_client(...) | Um cliente separado que também recorre ao ambiente, para uma segunda chave ao lado da partilhada, ou para um cliente que o seu próprio código guarda e passa de um lado para o outro. |
| OpenEmail::Client.new(...) or OpenEmail::Client.new(api_key) | Um cliente separado construído exatamente a partir do que passar. Não lê o ambiente, pelo que precisa de api_key: ou access_token:. OpenEmail.new é a mesma chamada. |
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)| Opção | Predefinição | Notas |
|---|---|---|
| api_key: | OPENEMAIL_API_KEY | Lida do ambiente por init, create_client e pelo cliente partilhado. Tem de começar por oe_live_ ou oe_test_. Também pode ser o primeiro argumento, mas não ambos. |
| access_token: | OPENEMAIL_ACCESS_TOKEN | Um token de acesso OAuth, ou qualquer objeto que responda a call e devolva um. Veja Tokens de acesso OAuth mais abaixo. Passe uma chave ou um token, nunca ambos. |
| base_url: | https://api.openemail.uk | Ou OPENEMAIL_BASE_URL. As barras finais são removidas, e init e create_client acrescentam https:// antes de um host simples, ou http:// antes de um host nesta máquina: localhost, um endereço 127.x.x.x ou ::1. Uma credencial nunca é enviada por http simples para outro host, e 0.0.0.0 ou [::] lança uma exceção quando o cliente é construído, porque são endereços onde um servidor escuta, não endereços para onde enviar pedidos. |
| timeout: | 30 | Segundos por tentativa, não por chamada. Com o adaptador predefinido, cobre a ligação e a leitura do corpo inteiro, não apenas dos cabeçalhos. 0 desativa-o. files.upload espera pelo menos 600 segundos, a menos que passe timeout: nessa chamada. |
| max_retries: | 2 | Tentativas adicionais após a primeira, em chamadas que é seguro repetir. Define-se no cliente, não por chamada. 0 desativa as repetições. |
| adapter: | OpenEmail::NetHttpAdapter.new | A camada HTTP. O adaptador predefinido mantém até 8 ligações inativas por host durante 2 segundos cada, e max_idle: e keep_alive_timeout: alteram isso. Qualquer objeto que responda a call(request) pode ocupar o seu lugar, e é assim que um teste corre sem rede. |
| headers: | {} | Enviados em todos os pedidos. |
| user_agent: | openemail-ruby/<version> | Enviados em todos os pedidos. |
| disable_update_notice: | false | Ignora a verificação, feita uma vez por processo, de uma versão mais recente no RubyGems. A verificação só é executada quando a saída padrão é um terminal, e OPENEMAIL_DISABLE_UPDATE_NOTICE também a desativa. |
Variáveis de ambiente
| Variável | O que faz |
|---|---|
| OPENEMAIL_API_KEY | A chave que init, create_client e o cliente partilhado usam quando não passa nem api_key: nem access_token:. |
| OPENEMAIL_ACCESS_TOKEN | Um token de acesso OAuth, lido apenas quando não passa nenhuma das duas credenciais e OPENEMAIL_API_KEY não está definida, por isso uma chave no ambiente tem prioridade. |
| OPENEMAIL_BASE_URL | O URL base quando não passa nenhum. A um host simples como localhost:2222 é acrescentado o esquema. |
| OPENEMAIL_DISABLE_UPDATE_NOTICE | Qualquer valor não vazio desativa o aviso de atualização, para todos os clientes do processo. |
| HTTPS_PROXY e NO_PROXY, ou https_proxy e no_proxy | O proxy através do qual o adaptador predefinido se liga, e os hosts que vão diretamente. Veja Proxies mais abaixo. |
OpenEmail::Client.new não lê nenhuma das três primeiras, por isso um cliente construído dessa forma nunca apanha por acidente uma chave do ambiente. Uma variável definida mas vazia conta como não definida.
O que recusa antes de enviar
Estes casos lançam ArgumentError a partir da linha que continha o valor errado, em vez de surgirem como uma falha confusa no seu primeiro envio. A mensagem indica o que estava errado e o que passar em vez disso, e nunca repete uma credencial.
| Recusado | Porquê |
|---|---|
| Nenhuma credencial | Não foi passado nem api_key: nem access_token:, e, para init e create_client, também nenhuma das duas variáveis estava definida, por isso não há nada com que autenticar. Lançado quando o cliente é construído. |
| Uma chave e um token em simultâneo | Cada pedido leva uma só credencial, por isso o cliente não consegue saber a qual se referia. Uma chave passada ao mesmo tempo como primeiro argumento e como api_key: é recusada pelo mesmo motivo. |
| Um cookie de sessão, um token de sessão ou uma chave de outro serviço | Só oe_live_ e oe_test_ autenticam aqui, e a API também o diz. A verificação é apenas de prefixo, pelo que uma chave revogada só é rejeitada quando o pedido é enviado, como OpenEmail::AuthenticationError. |
| Um base_url: que não é um URL http ou https, ou que contém um nome de utilizador ou uma palavra-passe | Não é possível chegar a mais nada, e uma credencial pertence a api_key: ou access_token:, não ao URL. Lançado quando o cliente é construído. |
| Uma credencial por http simples para um host que não está nesta máquina | Lançado pela chamada, antes de qualquer envio. Use um URL base https. |
| Um timeout: que não é um número de segundos, ou que é negativo | Passe segundos, ou 0 para não ter timeout. Lançado quando o cliente é construído. |
| Um nome de cabeçalho que não é um token, ou uma quebra de linha no valor de um cabeçalho | Verificado em headers:, user_agent: e idempotency_key:, porque uma quebra de linha iniciaria um segundo cabeçalho. |
| Um id vazio ou composto só por pontos em qualquer método | Lançado quando o método é chamado. Um segmento de caminho feito de pontos é removido por qualquer parser de URL, pelo que o pedido chegaria a um endpoint diferente. Um id que não seja UTF-8 válido também é recusado. |
| Um corpo de pedido que não é um Hash | Passe argumentos nomeados ou um único Hash. Qualquer objeto que responda a to_hash conta como tal. |
Não existe uma opção test_mode: nem vai existir. O esquema da chave faz parte da credencial e não é uma mera indicação, pelo que o modo é uma propriedade da chave. client.mode lê o prefixo, "live" ou "test", e não decide nada.
Um cliente, várias chaves
Construa o cliente uma vez e partilhe-o. Um cliente novo por pedido deita fora as suas ligações abertas sem qualquer ganho, e nenhum do estado que contém é específico de quem chama. Um cliente fica congelado depois de construído e pode ser usado com segurança a partir de muitas threads ao mesmo tempo, por isso um processo Puma ou Sidekiq só precisa de um, e depois de um fork o processo filho abre as suas próprias ligações.
No caso que, de outro modo, obrigaria a um cliente por chave, como uma tarefa que envia em nome de vários espaços de trabalho, passe api_key: na chamada. Substitui o cabeçalho Authorization para esse pedido e não deixa nada no cliente.
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 os métodos fora de temp_mail recebem-na como argumento nomeado, ao lado dos filtros de uma lista, e os métodos de temp_mail recebem antes inbox_token:. É verificada antes de o pedido ser enviado, pela mesma regra que o cliente usa, pelo que um erro de escrita lança um ArgumentError sobre a api_key passada nesta chamada em vez de um 401 sobre uma credencial que depois tem de ir procurar. Uma chamada repetida mantém a chave que lhe foi dada.
client.mode descreve a chave com que o cliente foi CONSTRUÍDO e não acompanha uma substituição. Quando um cliente serve várias chaves, não há um modo único a indicar, por isso leia-o a partir da chave que passou. client.inspect mostra o modo e o URL base, nunca a chave.
Endpoints que nenhum método envolve
client.raw é o transporte por onde passa cada método. client.raw.request chama um caminho que nenhum método envolve ainda, aplicando a credencial, o URL base, o timeout e a política de repetições do cliente, e devolve o corpo analisado tal como um método faz.
ping = client.raw.request("/ping") label = client.raw.request("/labels", method: :post, body: {name: "Invoices"}) p ping[:ok], label[:id]| Argumento | O que faz |
|---|---|
| method: | :get, a menos que indique outro: :post, :put, :patch ou :delete. |
| query: | Um Hash de parâmetros de consulta. Os valores nil e vazios são omitidos, um Array ou um Set é unido com vírgulas, e um Time é enviado como um instante ISO 8601. |
| body: | Um Hash, enviado como JSON. |
| raw: e content_type: | Bytes a enviar tal como estão, como String binária, IO ou Pathname, com application/octet-stream a menos que indique um tipo. |
| accept: e binary: | Um accept: diferente de JSON devolve o corpo como texto, e binary: true devolve-o como String binária. |
| idempotent: e idempotency_key: | idempotent: true anexa uma Idempotency-Key, gerada a menos que passe a sua. |
| repeatable: | Se uma falha é repetida. Só um GET o é, a menos que passe repeatable: true. |
| api_key: e timeout: | A mesma chave por chamada, e um timeout em segundos só para esta chamada. |
O caminho tem de começar por uma única /, e um caminho cujo URL final sairia da origem do URL base lança ArgumentError antes de qualquer envio, por isso a credencial nunca chega a outro host.
Caixas de entrada descartáveis
OpenEmail.create_temp_mail constrói um cliente para caixas descartáveis que não transporta nenhuma chave de API nem lê nenhuma do ambiente. Cria caixas de entrada de forma anónima, e cada leitura envia o token da caixa que create devolveu, ou o mais recente que extend devolveu, seja por chamada como inbox_token:, seja uma única vez como OpenEmail.create_temp_mail(inbox_token:).
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_atcreate_temp_mail aceita base_url:, adapter:, max_retries:, timeout:, user_agent:, headers: e disable_update_notice: como qualquer cliente, e lê OPENEMAIL_BASE_URL quando não passa nenhum URL base.
Tokens de acesso OAuth
Uma aplicação que alguém ligou por OAuth, como uma ferramenta de linha de comandos ou um agente, tem um token de acesso em vez de uma chave de API. Passe-o como access_token:, seja o próprio token, seja qualquer objeto que responda a call e o devolva, como uma lambda ou um Method. É chamado uma vez por cada chamada, e as repetições dessa chamada reutilizam o que ele devolveu, por isso renove o token dentro dele quando estiver perto de expirar e o cliente nunca terá de ser reconstruído.
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"| Caso | O que acontece |
|---|---|
| api_key: e access_token: juntos, ou nenhum | O cliente lança ArgumentError quando é construído. Sem nenhum dos dois, a mensagem refere OPENEMAIL_API_KEY e OPENEMAIL_ACCESS_TOKEN. |
| Um valor que não é um token | Um token tem de 1 a 512 caracteres e não começa por oe_, a verificação que OpenEmail.access_token? faz. Uma String que falha lança uma exceção quando o cliente é construído, e um objeto invocável que devolva uma assim lança ArgumentError a partir da chamada antes de qualquer envio. |
| OPENEMAIL_ACCESS_TOKEN | Lido por init, create_client e pelo cliente partilhado quando não passa nenhuma das duas credenciais e OPENEMAIL_API_KEY não está definida, por isso uma chave no ambiente tem prioridade. |
| Um invocável que lança uma exceção | A chamada lança esse mesmo erro, sem alterações, e nada é enviado. |
| Um api_key: por chamada | Substitui o token nesse único pedido, e o invocável não é chamado. |
| client.mode | Sempre "live" com um token. |
| OpenEmail.create_temp_mail | Não envia nenhuma credencial, seja o que for que o ambiente contenha. |
| me.get e me.ping | Para um token, get responde com object igual a oauth_token, id e roleId a nil, o clientId da aplicação ligada e expiresAt, o momento em que expira a aprovação que a pessoa deu à aplicação. ping responde com kind igual a oauth, keyId a nil e o clientId. Verifique object ou kind antes de ler id ou keyId. |
Um token atua em nome de uma pessoa e lê o correio dela como ela o pode ler, por isso mantenha-o num servidor, tal como uma chave.
Códigos de verificação
Antes de uma alteração sensível, como apagar um domínio ou alterar um webhook, a API pede a um token de acesso o código de verificação que a aplicação web pediria à pessoa. A chamada lança um OpenEmail::PermissionError, um 403 cujo step_up_required? é true, e nada foi alterado. Peça um código, verifique o que a pessoa lhe der e depois faça a chamada de novo. A uma chave de API nunca é pedido.
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étodo | O que faz |
|---|---|
| security.step_up_status | Se a aplicação está verificada neste momento (elevated, elevatedUntil), como é verificado o próximo código (method, email ou totp) e minutes, a duração da janela. Não envia nada e não indica uma pausa. |
| security.begin_step_up | Abre um desafio. Com email segue um código de seis dígitos para o endereço com que a pessoa inicia sessão, e sentTo mostra-o mascarado. Com totp a pessoa lê um na sua aplicação de autenticação ou usa um código de recuperação. Um desafio que ainda esteja aberto e tenha tentativas é reutilizado, a não ser que passe resend: true, e um bloqueado ou expirado é substituído por uma chamada simples. Cada aplicação pode abrir 5 por hora e 20 em 24 horas para cada pessoa, e o seguinte lança um 429 step_up_throttled. |
| security.verify_step_up(code:) | Verifica o código e desbloqueia as alterações sensíveis para esta aplicação durante 60 minutos, até elevatedUntil, por REST e através das ferramentas MCP que fazem as mesmas alterações. Depois de 10 códigos errados em 24 horas desta aplicação, ou 20 de todas as aplicações da pessoa juntas, esta chamada e begin_step_up lançam um 429 step_up_locked com uma mensagem que diz quando a verificação é retomada. |
O cliente nunca pede um código nem repete a chamada por si só, e nenhum dos três métodos é repetido automaticamente, porque uma repetição depois de uma resposta perdida poderia enviar um segundo email ou gastar uma segunda tentativa. Não precisam de âmbito, e uma chave de API que chame um deles recebe um 400 step_up_not_applicable. OpenEmail::STEP_UP_ERROR_CODES indica todas as formas como uma verificação pode falhar, e a página de erros da API diz o que fazer em cada caso.
O aviso de atualização
Quando há uma versão mais recente da gem no RubyGems, o cliente di-lo uma vez por processo, na saída de erro padrão, com uma linha como ℹ openemail 0.0.2 is available, you are on 0.0.1. seguida da página da gem. A verificação é executada quando o primeiro cliente é construído, numa thread em segundo plano com um timeout de dois segundos, só quando a saída padrão é um terminal, e uma falha ao chegar ao RubyGems é ignorada.
A verificação passa pelo adaptador do cliente, por isso um adaptador de teste pode ver um pedido ao RubyGems quando os testes correm num terminal. Construa os clientes de teste com disable_update_notice: true, ou defina OPENEMAIL_DISABLE_UPDATE_NOTICE.
Proxies
O adaptador predefinido encontra o seu proxy com o próprio URI#find_proxy do Ruby, por isso segue as mesmas regras que o resto da biblioteca padrão: https_proxy ou HTTPS_PROXY indica o proxy, e no_proxy ou NO_PROXY lista os hosts que vão diretamente. Um nome de utilizador e uma palavra-passe no URL do proxy são enviados ao proxy, e nunca se chega a um servidor nesta máquina através de um proxy.
As ligações usam TLS 1.2 ou posterior e verificam o certificado do servidor, por isso um proxy que inspeciona TLS precisa de que o OpenSSL da máquina confie na sua autoridade de certificação.
Testar sem rede
adapter: substitui a camada HTTP. É qualquer objeto que responda a call(request), incluindo uma lambda, e devolva um OpenEmail::HttpResponse com status, headers e body. O pedido é um OpenEmail::HttpRequest com method, url, headers, body e timeout, por isso um teste pode verificar exatamente o que teria saído.
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.firstAo imprimir um pedido, o seu cabeçalho Authorization aparece como [redacted], por isso um log de testes nunca contém a chave.
- Devolva um estado fora de 2xx com o envelope de erro da API como corpo, por exemplo
{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}, para obter a subclasse deOpenEmail::ApiErrorcorrespondente. - Lance
Timeout::Errora partir decall, ouNet::ReadTimeout, que é um deles, para obter umOpenEmail::NetworkErrorcujotimeout?é true. Qualquer outro StandardError, comoErrno::ECONNREFUSED, torna-se umNetworkErrorcujotimeout?é false. NameError,TypeErroreArgumentErrorlançados dentro do adaptador contam como bugs dele. São lançados sem alterações e nunca são repetidos.
Construa um cliente de teste com max_retries: 0 quando simular falhas. Caso contrário, um estado que admite repetição ou uma falha de rede numa chamada que é seguro repetir é tentado três vezes, com esperas reais pelo meio.