設定
クライアントを作る 3 つの方法、すべてのオプション、そしてリクエスト送信前に拒否されるもの。
オプション
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| エントリーポイント | 得られるもの |
|---|---|
| OpenEmail.init(...) | 共有クライアントを設定して返します。以後 OpenEmail.client はどのファイル、どのスレッドでもそのクライアントになり、省略したものは環境変数から読まれます。 |
| OpenEmail.client、OpenEmail.emails、OpenEmail.threads をはじめとするすべての名前空間 | 共有クライアントと、その名前空間へのショートカット。init より前に使うと、最初の呼び出し時に OPENEMAIL_API_KEY と OPENEMAIL_BASE_URL から自分を組み立てます。 |
| OpenEmail.reset_client | 共有クライアントを破棄し、次の呼び出しで環境変数から新しいクライアントが作られるようにします。 |
| OpenEmail.create_client(...) | 同じように環境変数へフォールバックする別のクライアント。共有クライアントとは別の 2 つ目のキーに使うか、自分のコードで保持して受け渡すクライアントに使います。 |
| OpenEmail::Client.new(...) or OpenEmail::Client.new(api_key) | 渡したものだけから作られる別のクライアント。環境変数を読まないため、api_key: か access_token: が必要です。OpenEmail.new も同じ呼び出しです。 |
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)| オプション | 既定値 | 備考 |
|---|---|---|
| api_key: | OPENEMAIL_API_KEY | init、create_client、共有クライアントが環境変数から読みます。oe_live_ か oe_test_ で始まる必要があります。最初の引数として渡すこともできますが、両方は指定できません。 |
| access_token: | OPENEMAIL_ACCESS_TOKEN | OAuth アクセストークン、または call に応答してトークンを返す任意のオブジェクト。下の「OAuth アクセストークン」を参照してください。キーかトークンのどちらかを渡し、両方は渡さないでください。 |
| base_url: | https://api.openemail.uk | または OPENEMAIL_BASE_URL。末尾のスラッシュは取り除かれ、init と create_client はホスト名だけの値には https:// を、このマシン上のホスト(localhost、127.x.x.x のアドレス、::1)には http:// を前置します。資格情報がそれ以外のホストへ暗号化されていない http で送られることはありません。また 0.0.0.0 や [::] はクライアントの作成時に例外を送出します。これらはサーバーが待ち受けるアドレスであり、リクエストの送信先ではないからです。 |
| timeout: | 30 | 呼び出しごとではなく、試行ごとの秒数です。既定のアダプターでは、ヘッダーだけでなく接続とボディ全体の読み取りまでを対象にします。0 で無効になります。files.upload は、その呼び出しで timeout: を渡さない限り少なくとも 600 秒待ちます。 |
| max_retries: | 2 | 繰り返しても安全な呼び出しについて、初回のあとの追加試行回数です。呼び出しごとではなくクライアントに設定します。0 でリトライを無効にします。 |
| adapter: | OpenEmail::NetHttpAdapter.new | HTTP 層。既定ではホストごとに最大 8 個のアイドル接続をそれぞれ 2 秒間保持し、max_idle: と keep_alive_timeout: でこれを変更できます。call(request) に応答するものなら何でも代わりに使え、テストはこの方法でネットワークなしに実行します。 |
| headers: | {} | すべてのリクエストに付与されます。 |
| user_agent: | openemail-ruby/<version> | すべてのリクエストに付与されます。 |
| disable_update_notice: | false | RubyGems 上の新しいバージョンをプロセスごとに 1 回確認する処理を省きます。確認は標準出力がターミナルのときだけ走り、OPENEMAIL_DISABLE_UPDATE_NOTICE でも無効にできます。 |
環境変数
| 変数 | 機能 |
|---|---|
| OPENEMAIL_API_KEY | api_key: も access_token: も渡さないときに、init、create_client、共有クライアントが使うキー。 |
| OPENEMAIL_ACCESS_TOKEN | OAuth アクセストークン。どちらの資格情報も渡さず、OPENEMAIL_API_KEY も設定されていないときにだけ読まれるため、環境変数にキーがあればそちらが優先されます。 |
| OPENEMAIL_BASE_URL | 何も渡さないときのベース URL。localhost:2222 のようなホスト名だけの値にはスキームが付け加えられます。 |
| OPENEMAIL_DISABLE_UPDATE_NOTICE | 空でない値であれば何でも、プロセス内のすべてのクライアントで更新通知を無効にします。 |
| HTTPS_PROXY と NO_PROXY、または https_proxy と no_proxy | 既定のアダプターが経由するプロキシと、直接接続するホスト。下の「プロキシ」を参照してください。 |
OpenEmail::Client.new は最初の 3 つのどれも読まないため、この方法で作ったクライアントが誤って環境変数からキーを拾うことはありません。設定されていても空の変数は、未設定として扱われます。
送信前に拒否されるもの
これらは、最初の送信で分かりにくい失敗として表面化するのではなく、誤った値を含む行から ArgumentError を送出します。メッセージには何が誤っていて代わりに何を渡すべきかが書かれ、資格情報がそのまま繰り返されることはありません。
| 拒否される条件 | 理由 |
|---|---|
| 資格情報がまったくない | api_key: も access_token: も渡されず、init と create_client ではどちらの変数も設定されていなかったため、認証に使うものがありません。クライアントの作成時に送出されます。 |
| キーとトークンを同時に指定 | どのリクエストも資格情報を 1 つしか運ばないため、クライアントにはどちらを意図したのか判断できません。最初の引数と api_key: の両方で渡されたキーも、同じ理由で拒否されます。 |
| セッション Cookie、セッショントークン、または別のサービスのキー | ここで認証できるのは oe_live_ と oe_test_ だけで、API もそう応答します。このチェックはプレフィックスを見るだけなので、失効したキーは実際に通信した段階で OpenEmail::AuthenticationError として失敗します。 |
| http または https の URL ではない base_url:、またはユーザー名やパスワードを含むもの | それ以外には接続できず、資格情報は URL ではなく api_key: か access_token: に入れるものです。クライアントの作成時に送出されます。 |
| このマシン上にないホストへ、暗号化されていない http で資格情報を送ろうとする | 何かが送信される前に、呼び出しが送出します。https のベース URL を使ってください。 |
| 秒数ではない、または負の timeout: | 秒数を渡すか、タイムアウトなしなら 0 を渡してください。クライアントの作成時に送出されます。 |
| トークンとして正しくないヘッダー名、またはヘッダー値に含まれる改行 | headers:、user_agent:、idempotency_key: でチェックされます。改行があると 2 つ目のヘッダーが始まってしまうからです。 |
| どのメソッドでも空、またはドットだけの id | メソッド呼び出し時に送出されます。ドットだけのパスセグメントはどの URL パーサーでも除去されるため、リクエストが別のエンドポイントに届いてしまいます。有効な UTF-8 ではない id も拒否されます。 |
| Hash ではないリクエストボディ | キーワード引数か、Hash を 1 つ渡してください。to_hash に応答するものは Hash として扱われます。 |
test_mode: というオプションはなく、今後も追加されません。キーの体系はヒントではなく資格情報の一部なので、モードはキーの属性です。client.mode はプレフィックスを読んで "live" か "test" を返すだけで、何も判断しません。
1 つのクライアント、複数のキー
クライアントは一度作って共有してください。リクエストごとに新しいクライアントを作ると、開いている接続を無駄に捨てることになり、しかもその状態に呼び出し元ごとのものは何もありません。クライアントは作成されると凍結され、多数のスレッドから同時に使っても安全なので、Puma や Sidekiq のプロセスには 1 つあれば足ります。fork 後は子プロセスが自分の接続を開きます。
複数のワークスペースに代わって送信するジョブのように、本来ならキーごとにクライアントが必要になる場合は、呼び出しで api_key: を渡してください。そのリクエストの Authorization ヘッダーを置き換え、クライアントには何も残しません。
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)temp_mail 以外のすべてのメソッドは、これをキーワード引数として受け取ります(一覧ではフィルターと並べて渡します)。temp_mail のメソッドは代わりに inbox_token: を受け取ります。リクエスト送信前に、クライアントと同じ規則でチェックされるため、タイプミスは、後で探し回ることになる資格情報についての 401 ではなく、この呼び出しに渡された api_key についての ArgumentError を送出します。リトライされた呼び出しは、渡されたキーをそのまま使い続けます。
client.mode はクライアントの「作成時」のキーを表し、上書きには追従しません。1 つのクライアントが複数のキーを扱うようになると、報告すべき単一のモードは存在しないため、渡したキーから読み取ってください。client.inspect はモードとベース URL を表示し、キーは決して表示しません。
どのメソッドもラップしていないエンドポイント
client.raw はすべてのメソッドが通るトランスポートです。client.raw.request は、まだどのメソッドもラップしていないパスを、クライアントの資格情報、ベース URL、タイムアウト、リトライ方針を適用して呼び出し、メソッドと同じようにパース済みのボディを返します。
ping = client.raw.request("/ping") label = client.raw.request("/labels", method: :post, body: {name: "Invoices"}) p ping[:ok], label[:id]| キーワード | 機能 |
|---|---|
| method: | 指定しない限り :get。ほかに :post、:put、:patch、:delete。 |
| query: | クエリパラメーターの Hash。nil と空の値は省かれ、Array や Set はカンマで連結され、Time は ISO 8601 の時刻として送られます。 |
| body: | JSON として送られる Hash。 |
| raw: と content_type: | そのまま送るバイト列。バイナリの String、IO、Pathname のいずれかで、型を指定しない限り application/octet-stream になります。 |
| accept: と binary: | JSON 以外の accept: はボディをテキストとして返し、binary: true はバイナリの String として返します。 |
| idempotent: と idempotency_key: | idempotent: true は Idempotency-Key を付与します。自分で渡さない限り自動生成されます。 |
| repeatable: | 失敗時にリトライするかどうか。repeatable: true を渡さない限り、リトライされるのは GET だけです。 |
| api_key: と timeout: | 呼び出しごとのキーと同じもの、およびこの呼び出しだけに適用される秒単位のタイムアウト。 |
パスは 1 つの / で始まる必要があります。完成した URL がベース URL のオリジンから外れるパスは、何かが送信される前に ArgumentError を送出するため、資格情報が別のホストに届くことはありません。
使い捨て受信トレイ
OpenEmail.create_temp_mail は、API キーを持たず環境変数からも読まない、使い捨て受信箱用のクライアントを作ります。受信箱は匿名で作成され、読み取りのたびに create が返した受信箱トークン、または extend が返した新しいトークンを送ります。トークンは呼び出しごとに inbox_token: として渡すか、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 は他のクライアントと同じく base_url:、adapter:、max_retries:、timeout:、user_agent:、headers:、disable_update_notice: を受け取り、ベース URL を渡さないときは OPENEMAIL_BASE_URL を読みます。
OAuthアクセストークン
コマンドラインツールやエージェントのように、人が OAuth で接続したアプリは、API キーではなくアクセストークンを持っています。これを access_token: として渡します。トークンそのものか、lambda や Method のように call に応答してトークンを返すものを渡せます。これは呼び出しごとに 1 回呼ばれ、その呼び出しのリトライは返された値を再利用するため、期限切れが近づいたらその中でトークンを更新すれば、クライアントを作り直す必要はありません。
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"| ケース | 起きること |
|---|---|
| api_key: と access_token: の両方、またはどちらもなし | クライアントの作成時に ArgumentError を送出します。どちらもない場合、メッセージは OPENEMAIL_API_KEY と OPENEMAIL_ACCESS_TOKEN に言及します。 |
| トークンではない値 | トークンは 1〜512 文字で、oe_ で始まりません。これが OpenEmail.access_token? の行うチェックです。これに通らない String はクライアントの作成時に例外を送出し、そうした値を返す callable は、何かが送信される前に呼び出しから ArgumentError を送出します。 |
| OPENEMAIL_ACCESS_TOKEN | どちらの資格情報も渡さず、OPENEMAIL_API_KEY も設定されていないときに、init、create_client、共有クライアントが読みます。そのため環境変数にキーがあればそちらが優先されます。 |
| 例外を送出する callable | 呼び出しはそのエラーをそのまま送出し、何も送信されません。 |
| 呼び出しごとの api_key: | そのリクエストに限ってトークンを置き換え、callable は呼ばれません。 |
| client.mode | トークンでは常に "live"。 |
| OpenEmail.create_temp_mail | 環境に何があっても、資格情報は送信しません。 |
| me.get と me.ping | トークンの場合、get は object が oauth_token、id と roleId が nil で、接続されたアプリの clientId と、本人によるアプリの承認が切れる時刻 expiresAt を返します。ping は kind が oauth、keyId が nil で、clientId を返します。id や keyId を読む前に object か kind を確認してください。 |
トークンは本人に代わって動作し、本人と同じようにメールを読めるため、キーと同様にサーバー上に置いてください。
確認コード
ドメインの削除や Webhook の変更などの重要な変更の前に、API はアクセストークンに対し、Web アプリが本人に求めるのと同じ確認コードを求めます。呼び出しは OpenEmail::PermissionError を送出します。これは step_up_required? が true の 403 で、何も変更されていません。コードを求め、本人から受け取ったコードを確認してから、もう一度呼び出してください。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| メソッド | 機能 |
|---|---|
| security.step_up_status | アプリがいま確認済みかどうか(elevated、elevatedUntil)、次のコードの確認方法(method、email または totp)、そして確認が有効な時間の長さである minutes。何も送信せず、一時停止も報告しません。 |
| security.begin_step_up | 確認を開始します。email では、本人がサインインに使うアドレスに 6 桁のコードが届き、sentTo がそれを伏せた形で示します。totp では、本人が認証アプリからコードを読むか、バックアップコードを使います。まだ開いていて試行回数が残っている確認は、resend: true を渡さない限り再利用され、ロックされたものや期限切れのものは通常の呼び出しで置き換えられます。各アプリは本人ごとに 1 時間に 5 回、24 時間に 20 回まで確認を開始でき、それを超えると 429 step_up_throttled を送出します。 |
| security.verify_step_up(code:) | コードを確認し、REST と、同じ変更を行う MCP ツールを通じて、このアプリの重要な変更を 60 分間、elevatedUntil まで許可します。このアプリから 24 時間に 10 回、または本人のすべてのアプリの合計で 20 回コードを誤ると、この呼び出しと begin_step_up は、確認をいつ再開できるかを示すメッセージ付きで 429 step_up_locked を送出します。 |
クライアントが自分からコードを求めたり、呼び出しをやり直したりすることはありません。3 つのメソッドはどれも自動ではリトライされません。応答が失われたあとのリトライで、2 通目のメールが送られたり、試行回数を余分に消費したりするおそれがあるからです。スコープは不要で、API キーでどれかを呼ぶと 400 step_up_not_applicable が返ります。OpenEmail::STEP_UP_ERROR_CODES は確認が失敗するすべての理由を挙げており、それぞれの対処は API のエラーページにあります。
更新通知
RubyGems に gem の新しいバージョンがあると、クライアントはプロセスごとに 1 回、標準エラーに ℹ openemail 0.0.2 is available, you are on 0.0.1. のような行を出力し、続けて gem のページを示します。このチェックは最初のクライアントの作成時に、2 秒のタイムアウト付きのバックグラウンドスレッドで、標準出力がターミナルのときだけ実行され、RubyGems に接続できなくても無視されます。
このチェックはクライアントのアダプターを経由するため、テストをターミナルで実行すると、テスト用アダプターに RubyGems へのリクエストが見えることがあります。テスト用クライアントは disable_update_notice: true で作るか、OPENEMAIL_DISABLE_UPDATE_NOTICE を設定してください。
プロキシ
既定のアダプターは Ruby 自身の URI#find_proxy でプロキシを見つけるため、標準ライブラリの他の部分と同じ規則に従います。https_proxy または HTTPS_PROXY がプロキシを指定し、no_proxy または NO_PROXY が直接接続するホストを列挙します。プロキシ URL に含まれるユーザー名とパスワードはプロキシに送られ、このマシン上のサーバーにプロキシ経由で接続することはありません。
接続には TLS 1.2 以降を使い、サーバーの証明書を検証します。そのため TLS を検査するプロキシを使う場合は、その認証局をマシン上の OpenSSL に信頼させる必要があります。
ネットワークなしでのテスト
adapter: は HTTP 層を置き換えます。call(request) に応答するものなら lambda を含め何でもよく、status、headers、body を持つ OpenEmail::HttpResponse を返します。リクエストは method、url、headers、body、timeout を持つ OpenEmail::HttpRequest なので、テストでは何が送信されるはずだったかを正確に確認できます。
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リクエストを出力すると Authorization ヘッダーは [redacted] と表示されるため、テストのログにキーが残ることはありません。
- 対応する
OpenEmail::ApiErrorのサブクラスを得るには、{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}のように、API のエラーエンベロープをボディにした 2xx 以外のステータスを返します。 timeout?が true のOpenEmail::NetworkErrorを得るには、callからTimeout::Error、またはその一種であるNet::ReadTimeoutを送出します。Errno::ECONNREFUSEDなど、それ以外の StandardError はtimeout?が false のNetworkErrorになります。- アダプター内で送出された
NameError、TypeError、ArgumentErrorは、アダプター自体のバグとして扱われます。そのまま送出され、リトライされることはありません。
失敗をシナリオとして組む場合は、テスト用クライアントを max_retries: 0 で作ってください。そうしないと、繰り返しても安全な呼び出しでのリトライ可能なステータスやネットワーク障害は 3 回試行され、その間に実際の sleep が入ります。