Configuration
Trois façons de construire un client, toutes les options, et ce qu'il refuse avant qu'une requête ne parte.
Options
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| Point d'entrée | Ce que vous obtenez |
|---|---|
| OpenEmail.init(...) | Configure le client partagé et le renvoie. OpenEmail.client est ce client à partir de là, dans chaque fichier et chaque thread, et tout ce que vous omettez est lu dans l'environnement. |
| OpenEmail.client, OpenEmail.emails, OpenEmail.threads et tous les autres espaces de noms | Le client partagé et des raccourcis vers ses espaces de noms. Utilisé avant init, il se construit lui-même au premier appel à partir de OPENEMAIL_API_KEY et OPENEMAIL_BASE_URL. |
| OpenEmail.reset_client | Abandonne le client partagé : le prochain appel en construit un nouveau à partir de l'environnement. |
| OpenEmail.create_client(...) | Un client distinct avec le même repli sur l'environnement, pour une deuxième clé à côté de celle du client partagé, ou pour un client que votre propre code détient et transmet. |
| OpenEmail::Client.new(...) or OpenEmail::Client.new(api_key) | Un client distinct construit exactement à partir de ce que vous passez. Il ne lit pas l'environnement : il lui faut donc api_key: ou access_token:. OpenEmail.new est le même appel. |
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)| Option | Par défaut | Remarques |
|---|---|---|
| api_key: | OPENEMAIL_API_KEY | Lu dans l'environnement par init, create_client et le client partagé. Doit commencer par oe_live_ ou oe_test_. Peut aussi être passé en premier argument, mais pas les deux. |
| access_token: | OPENEMAIL_ACCESS_TOKEN | Un jeton d'accès OAuth, ou tout objet qui répond à call et en renvoie un. Voir Jetons d'accès OAuth plus bas. Passez une clé ou un jeton, jamais les deux. |
| base_url: | https://api.openemail.uk | Ou OPENEMAIL_BASE_URL. Les barres obliques finales sont supprimées, et init et create_client ajoutent https:// devant un hôte nu, ou http:// devant un hôte de cette machine : localhost, une adresse 127.x.x.x ou ::1. Un identifiant n'est jamais envoyé en http simple vers un autre hôte, et 0.0.0.0 ou [::] lève une erreur à la construction du client, car ce sont des adresses sur lesquelles un serveur écoute, pas des adresses auxquelles envoyer des requêtes. |
| timeout: | 30 | Secondes par tentative, pas par appel. Avec l'adaptateur par défaut, cela couvre la connexion et la lecture du corps entier, pas seulement des en-têtes. 0 le désactive. files.upload attend au moins 600 secondes, sauf si vous passez timeout: sur cet appel. |
| max_retries: | 2 | Tentatives supplémentaires après la première, sur les appels qui peuvent être répétés sans risque. Se règle sur le client, pas par appel. 0 désactive les réessais. |
| adapter: | OpenEmail::NetHttpAdapter.new | La couche HTTP. Celle par défaut garde jusqu'à 8 connexions inactives par hôte, 2 secondes chacune, et max_idle: et keep_alive_timeout: modifient cela. Tout objet qui répond à call(request) peut la remplacer : c'est ainsi qu'un test s'exécute sans réseau. |
| headers: | {} | Envoyés à chaque requête. |
| user_agent: | openemail-ruby/<version> | Envoyés à chaque requête. |
| disable_update_notice: | false | Ignore la vérification, faite une fois par processus, d'une version plus récente sur RubyGems. La vérification ne s'exécute que lorsque la sortie standard est un terminal, et OPENEMAIL_DISABLE_UPDATE_NOTICE la désactive aussi. |
Variables d'environnement
| Variable | Ce qu'il fait |
|---|---|
| OPENEMAIL_API_KEY | La clé qu'utilisent init, create_client et le client partagé quand vous ne passez ni api_key: ni access_token:. |
| OPENEMAIL_ACCESS_TOKEN | Un jeton d'accès OAuth, lu seulement quand vous ne passez aucun des deux identifiants et que OPENEMAIL_API_KEY n'est pas définie : une clé présente dans l'environnement l'emporte donc. |
| OPENEMAIL_BASE_URL | L'URL de base quand vous n'en passez aucune. Un hôte nu comme localhost:2222 reçoit son schéma. |
| OPENEMAIL_DISABLE_UPDATE_NOTICE | Toute valeur non vide désactive l'avis de mise à jour, pour tous les clients du processus. |
| HTTPS_PROXY et NO_PROXY, ou https_proxy et no_proxy | Le proxy par lequel se connecte l'adaptateur par défaut, et les hôtes joints directement. Voir Proxys plus bas. |
OpenEmail::Client.new ne lit aucune des trois premières : un client construit ainsi ne récupère donc jamais une clé de l'environnement par accident. Une variable définie mais vide compte comme non définie.
Ce qu'il refuse avant d'envoyer
Ces cas lèvent ArgumentError depuis la ligne qui contenait la mauvaise valeur, au lieu d'apparaître comme un échec déroutant lors de votre premier envoi. Le message indique ce qui n'allait pas et ce qu'il faut passer à la place, et il ne répète jamais un identifiant.
| Refusé | Pourquoi |
|---|---|
| Aucun identifiant | Ni api_key: ni access_token: n'a été passé, et pour init et create_client aucune des deux variables n'était définie non plus : il n'y a donc rien pour s'authentifier. Levée à la construction du client. |
| Une clé et un jeton ensemble | Chaque requête porte un seul identifiant : le client ne peut donc pas savoir lequel vous vouliez. Une clé passée à la fois en premier argument et en api_key: est refusée pour la même raison. |
| Un cookie de session, un jeton de session ou une clé d'un autre service | Seuls oe_live_ et oe_test_ authentifient ici, et l'API le dit aussi. La vérification porte sur le préfixe et rien de plus : une clé révoquée échoue donc quand même au moment de la requête, sous la forme d'une OpenEmail::AuthenticationError. |
| Une base_url: qui n'est pas une URL http ou https, ou qui contient un nom d'utilisateur ou un mot de passe | Rien d'autre n'est joignable, et un identifiant a sa place dans api_key: ou access_token:, pas dans l'URL. Levée à la construction du client. |
| Un identifiant envoyé en http simple vers un hôte qui n'est pas sur cette machine | Levée par l'appel, avant tout envoi. Utilisez une URL de base en https. |
| Un timeout: qui n'est pas un nombre de secondes, ou qui est négatif | Passez des secondes, ou 0 pour aucun délai. Levée à la construction du client. |
| Un nom d'en-tête qui n'est pas un token valide, ou un saut de ligne dans une valeur d'en-tête | Vérifié dans headers:, user_agent: et idempotency_key:, car un saut de ligne commencerait un deuxième en-tête. |
| Un id vide ou fait uniquement de points sur n'importe quelle méthode | Levée à l'appel de la méthode. Un segment de chemin fait de points est supprimé par tous les analyseurs d'URL : la requête atteindrait donc un autre endpoint. Un id qui n'est pas de l'UTF-8 valide est également refusé. |
| Un corps de requête qui n'est pas un Hash | Passez des arguments nommés ou un seul Hash. Tout objet qui répond à to_hash compte comme un Hash. |
Il n'existe pas d'option test_mode: et il n'y en aura pas. Le schéma de la clé fait partie de l'identifiant et n'est pas un simple indice : le mode est donc une propriété de la clé. client.mode lit le préfixe, "live" ou "test", et ne décide de rien.
Un client, plusieurs clés
Construisez le client une fois et partagez-le. Un nouveau client par requête jette ses connexions ouvertes pour rien, et aucun de ses états n'est propre à un appelant. Un client est gelé une fois construit et peut être utilisé sans risque depuis de nombreux threads à la fois : un processus Puma ou Sidekiq n'en a donc besoin que d'un, et après un fork, le processus enfant ouvre ses propres connexions.
Pour le cas qui imposerait sinon un client par clé, comme un job qui envoie pour le compte de plusieurs espaces de travail, passez api_key: sur l'appel. Il remplace l'en-tête Authorization pour cette requête et ne laisse rien derrière lui sur le client.
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)Toute méthode hors de temp_mail le prend comme argument nommé, à côté des filtres d'une liste, et les méthodes de temp_mail prennent inbox_token: à la place. Il est vérifié avant l'envoi de la requête, selon la même règle que celle du client : une faute de frappe lève donc une ArgumentError portant sur l'api_key passée à cet appel, plutôt qu'un 401 sur un identifiant qu'il vous faudrait ensuite retrouver. Un appel réessayé garde la clé qui lui a été donnée.
client.mode décrit la clé avec laquelle le client a été CONSTRUIT et ne suit pas une substitution. Dès qu'un client sert plusieurs clés, il n'y a plus de mode unique à signaler : lisez-le donc sur la clé que vous avez passée. client.inspect affiche le mode et l'URL de base, jamais la clé.
Endpoints qu'aucune méthode n'encapsule
client.raw est le transport par lequel passe chaque méthode. client.raw.request appelle un chemin qu'aucune méthode n'encapsule encore, en appliquant l'identifiant, l'URL de base, le délai et la politique de réessai du client, et renvoie le corps analysé comme le fait une méthode.
ping = client.raw.request("/ping") label = client.raw.request("/labels", method: :post, body: {name: "Invoices"}) p ping[:ok], label[:id]| Mot-clé | Ce qu'il fait |
|---|---|
| method: | :get sauf indication contraire : :post, :put, :patch ou :delete. |
| query: | Un Hash de paramètres de requête. Les valeurs nil et vides sont omises, un Array ou un Set est joint par des virgules, et un Time est envoyé comme un instant ISO 8601. |
| body: | Un Hash, envoyé en JSON. |
| raw: et content_type: | Des octets à envoyer tels quels, sous forme de String binaire, d'IO ou de Pathname, avec application/octet-stream sauf si vous indiquez un type. |
| accept: et binary: | Un accept: autre que JSON renvoie le corps sous forme de texte, et binary: true le renvoie sous forme de String binaire. |
| idempotent: et idempotency_key: | idempotent: true attache un Idempotency-Key, généré sauf si vous passez le vôtre. |
| repeatable: | Indique si un échec est réessayé. Seul un GET l'est, sauf si vous passez repeatable: true. |
| api_key: et timeout: | La même clé par appel, et un délai en secondes pour ce seul appel. |
Le chemin doit commencer par un seul /, et un chemin dont l'URL finale quitterait l'origine de l'URL de base lève ArgumentError avant tout envoi : l'identifiant n'atteint donc jamais un autre hôte.
Boîtes jetables
OpenEmail.create_temp_mail construit un client pour boîtes jetables qui ne porte aucune clé API et n'en lit aucune dans l'environnement. Il crée des boîtes anonymement, et chaque lecture envoie le jeton de boîte renvoyé par create, ou le plus récent renvoyé par extend, soit par appel via inbox_token:, soit une fois via 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 prend base_url:, adapter:, max_retries:, timeout:, user_agent:, headers: et disable_update_notice: comme n'importe quel client, et lit OPENEMAIL_BASE_URL quand vous ne passez pas d'URL de base.
Jetons d'accès OAuth
Une application qu'une personne a connectée en OAuth, comme un outil en ligne de commande ou un agent, détient un jeton d'accès plutôt qu'une clé API. Passez-le comme access_token:, soit le jeton lui-même, soit tout objet qui répond à call et le renvoie, comme une lambda ou une Method. Cet objet est appelé une fois par appel, et les réessais de cet appel réutilisent ce qu'il a renvoyé : renouvelez donc le jeton à l'intérieur quand il approche de son expiration, et le client n'aura jamais à être reconstruit.
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"| Cas | Ce qui se passe |
|---|---|
| api_key: et access_token: ensemble, ou aucun des deux | Le client lève ArgumentError à sa construction. Sans aucun des deux, le message nomme OPENEMAIL_API_KEY et OPENEMAIL_ACCESS_TOKEN. |
| Une valeur qui n'est pas un jeton | Un jeton fait de 1 à 512 caractères et ne commence pas par oe_, la vérification que fait OpenEmail.access_token?. Une String qui ne la passe pas lève une erreur à la construction du client, et un callable qui en renvoie une lève ArgumentError depuis l'appel, avant tout envoi. |
| OPENEMAIL_ACCESS_TOKEN | Lu par init, create_client et le client partagé quand vous ne passez aucun des deux identifiants et que OPENEMAIL_API_KEY n'est pas définie : une clé dans l'environnement l'emporte donc. |
| Un callable qui lève une erreur | L'appel lève cette erreur, inchangée, et rien n'est envoyé. |
| Une api_key: par appel | Remplace le jeton pour cette seule requête, et le callable n'est pas appelé. |
| client.mode | Toujours "live" avec un jeton. |
| OpenEmail.create_temp_mail | N'envoie aucun identifiant, quel que soit le contenu de l'environnement. |
| me.get et me.ping | Pour un jeton, get répond avec object à oauth_token, id et roleId à nil, le clientId de l'application connectée, et expiresAt, le moment où l'autorisation donnée par la personne à l'application expire. ping répond avec kind à oauth, keyId à nil et le clientId. Vérifiez object ou kind avant de lire id ou keyId. |
Un jeton agit pour une personne et lit son courrier comme elle le peut : gardez-le donc sur un serveur, comme une clé.
Codes de vérification
Avant un changement sensible, comme supprimer un domaine ou modifier un webhook, l'API demande à un jeton d'accès le code de vérification que l'application web demanderait à la personne. L'appel lève une OpenEmail::PermissionError, un 403 dont step_up_required? vaut true, et rien n'a été modifié. Demandez un code, vérifiez celui que la personne vous donne, puis refaites l'appel. On ne le demande jamais à une clé API.
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éthode | Ce qu'il fait |
|---|---|
| security.step_up_status | Si l'application est vérifiée en ce moment (elevated, elevatedUntil), comment le prochain code est contrôlé (method, email ou totp), et minutes, la durée de la fenêtre. N'envoie rien et ne signale pas de pause. |
| security.begin_step_up | Ouvre une vérification. Avec email, un code à six chiffres part vers l'adresse avec laquelle la personne se connecte, et sentTo l'affiche masquée. Avec totp, elle en lit un dans son application d'authentification ou utilise un code de récupération. Une vérification encore ouverte à laquelle il reste des essais est réutilisée, sauf si vous passez resend: true, et une vérification verrouillée ou expirée est remplacée par un simple appel. Chaque application peut en ouvrir 5 par heure et 20 en 24 heures pour chaque personne, et la suivante lève un 429 step_up_throttled. |
| security.verify_step_up(code:) | Contrôle le code et débloque les changements sensibles pour cette application pendant 60 minutes, jusqu'à elevatedUntil, par REST et par les outils MCP qui font les mêmes changements. Après 10 codes erronés en 24 heures venant de cette application, ou 20 venant de toutes les applications de la personne ensemble, cet appel et begin_step_up lèvent un 429 step_up_locked avec un message qui indique quand la vérification reprend. |
Le client ne demande jamais de code et ne refait jamais l'appel de lui-même, et aucune des trois méthodes n'est réessayée automatiquement, car une nouvelle tentative après une réponse perdue pourrait envoyer un deuxième e-mail ou consommer un deuxième essai. Elles ne demandent aucune portée, et une clé API qui en appelle une reçoit un 400 step_up_not_applicable. OpenEmail::STEP_UP_ERROR_CODES nomme chaque façon dont une vérification peut échouer, et la page des erreurs de l'API indique quoi faire dans chaque cas.
L'avis de mise à jour
Quand une version plus récente de la gem est disponible sur RubyGems, le client le signale une fois par processus, sur la sortie d'erreur standard, par une ligne comme ℹ openemail 0.0.2 is available, you are on 0.0.1. suivie de la page de la gem. La vérification s'exécute à la construction du premier client, dans un thread d'arrière-plan avec un délai de deux secondes, seulement quand la sortie standard est un terminal, et un échec pour joindre RubyGems est ignoré.
La vérification passe par l'adaptateur du client : un adaptateur de test peut donc voir une requête vers RubyGems quand les tests s'exécutent dans un terminal. Construisez les clients de test avec disable_update_notice: true, ou définissez OPENEMAIL_DISABLE_UPDATE_NOTICE.
Proxys
L'adaptateur par défaut trouve son proxy avec URI#find_proxy, propre à Ruby : il suit donc les mêmes règles que le reste de la bibliothèque standard. https_proxy ou HTTPS_PROXY nomme le proxy, et no_proxy ou NO_PROXY liste les hôtes joints directement. Un nom d'utilisateur et un mot de passe dans l'URL du proxy sont envoyés au proxy, et un serveur sur cette machine n'est jamais joint par un proxy.
Les connexions utilisent TLS 1.2 ou une version ultérieure et vérifient le certificat du serveur : un proxy qui inspecte le TLS a donc besoin que son autorité de certification soit approuvée par OpenSSL sur la machine.
Tester sans réseau
adapter: remplace la couche HTTP. C'est tout objet qui répond à call(request), lambda comprise, et renvoie une OpenEmail::HttpResponse avec status, headers et body. La requête est une OpenEmail::HttpRequest avec method, url, headers, body et timeout : un test peut donc vérifier exactement ce qui serait parti.
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.firstL'affichage d'une requête montre son en-tête Authorization sous la forme [redacted] : un journal de test ne contient donc jamais la clé.
- Renvoyez un statut hors de 2xx avec l'enveloppe d'erreur de l'API comme corps, par exemple
{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}, pour obtenir la sous-classe d'OpenEmail::ApiErrorcorrespondante. - Levez
Timeout::Errordepuiscall, ouNet::ReadTimeout, qui en est une, pour obtenir uneOpenEmail::NetworkErrordonttimeout?vaut true. Toute autre StandardError, commeErrno::ECONNREFUSED, devient uneNetworkErrordonttimeout?vaut false. NameError,TypeErroretArgumentErrorlevées dans l'adaptateur comptent comme des bugs de celui-ci. Elles sont relevées telles quelles et jamais réessayées.
Construisez un client de test avec max_retries: 0 quand vous scénarisez des échecs. Sinon, un statut réessayable ou un échec réseau sur un appel qui peut être répété sans risque est tenté trois fois, avec de vraies attentes entre les tentatives.