구성
클라이언트를 만드는 세 가지 방법, 모든 옵션, 그리고 요청을 보내기 전에 거부하는 것들.
옵션
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(...) | 같은 환경 변수 대체 동작을 갖춘 별도의 클라이언트로, 공유 클라이언트와 나란히 두 번째 키를 쓰거나, 자신의 코드가 보관하고 전달하는 클라이언트에 사용합니다. |
| 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에 새 버전이 있는지 프로세스당 한 번 확인하는 절차를 건너뜁니다. 이 확인은 표준 출력이 터미널일 때만 실행되며, 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는 처음 세 가지를 하나도 읽지 않으므로, 그렇게 만든 클라이언트가 실수로 환경 변수의 키를 가져가는 일은 없습니다. 설정되었지만 비어 있는 변수는 설정되지 않은 것으로 간주합니다.
보내기 전에 거부하는 것
이들은 첫 발송에서 알기 어려운 실패로 나타나는 대신, 잘못된 값이 들어간 바로 그 줄에서 ArgumentError를 발생시킵니다. 메시지는 무엇이 잘못되었고 대신 무엇을 전달해야 하는지 알려 주며, 자격 증명을 되풀이해 보여 주지 않습니다.
| 거부되는 것 | 이유 |
|---|---|
| 자격 증명이 전혀 없음 | api_key:도 access_token:도 전달되지 않았고, init과 create_client의 경우 두 환경 변수도 설정되지 않아 인증할 수단이 없습니다. 클라이언트를 만들 때 발생합니다. |
| 키와 토큰을 함께 전달함 | 모든 요청은 자격 증명 하나만 담으므로, 클라이언트는 어느 것을 의도했는지 알 수 없습니다. 첫 번째 인자와 api_key:로 동시에 전달한 키도 같은 이유로 거부됩니다. |
| 세션 쿠키, 세션 토큰, 또는 다른 서비스의 키 | 여기서 인증되는 것은 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:에서 검사합니다. 줄바꿈이 있으면 두 번째 헤더가 시작되기 때문입니다. |
| 메서드에 빈 id나 점으로만 이루어진 id를 전달 | 메서드를 호출할 때 발생합니다. 점으로 이루어진 경로 세그먼트는 모든 URL 파서가 제거하므로, 요청이 다른 엔드포인트에 도달하게 됩니다. 올바른 UTF-8이 아닌 id도 거부됩니다. |
| Hash가 아닌 요청 본문 | 키워드 인자나 Hash 하나를 전달하세요. to_hash에 응답하는 것은 무엇이든 Hash로 간주합니다. |
test_mode: 옵션은 없고 앞으로도 없을 것입니다. 키 체계는 힌트가 아니라 자격 증명의 일부이므로, 모드는 키의 속성입니다. client.mode는 접두사를 읽어 "live" 또는 "test"를 알려 줄 뿐 아무것도 결정하지 않습니다.
하나의 클라이언트, 여러 개의 키
클라이언트는 한 번 만들어 공유하세요. 요청마다 새 클라이언트를 만들면 열린 연결을 헛되이 버리는 셈이고, 클라이언트에 담긴 상태 중 호출자별로 달라지는 것은 없습니다. 클라이언트는 만들어진 뒤 동결되며 여러 스레드에서 동시에 사용해도 안전하므로, Puma나 Sidekiq 프로세스에는 하나만 있으면 되고, 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는 클라이언트를 만들 때 사용한 키를 설명하며, 호출별 재정의를 따라가지 않습니다. 하나의 클라이언트가 여러 키를 쓰는 순간 보고할 단일 모드가 없어지므로, 전달한 키에서 직접 읽으세요. 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: | 같은 호출별 키와, 이 호출에만 적용되는 초 단위 타임아웃입니다. |
경로는 / 하나로 시작해야 하며, 완성된 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에 응답해 토큰을 반환하는 객체입니다. 이 객체는 호출마다 한 번 불리고, 그 호출의 재시도는 반환된 값을 다시 씁니다. 그러니 만료가 가까워지면 그 안에서 토큰을 갱신하세요. 클라이언트를 다시 만들 필요가 없습니다.
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은 클라이언트를 만들 때 예외를 발생시키고, 그런 값을 반환하는 호출 가능 객체는 아무것도 보내기 전에 호출에서 ArgumentError를 발생시킵니다. |
| OPENEMAIL_ACCESS_TOKEN | 두 자격 증명 중 어느 것도 전달하지 않고 OPENEMAIL_API_KEY도 설정되지 않았을 때 init, create_client, 공유 클라이언트가 읽습니다. 즉 환경 변수의 키가 우선합니다. |
| 예외를 발생시키는 호출 가능 객체 | 호출은 그 예외를 바꾸지 않고 그대로 발생시키며, 아무것도 보내지 않습니다. |
| 호출별 api_key: | 그 요청 하나에 한해 토큰을 대체하고, 호출 가능 객체는 호출되지 않습니다. |
| 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를 확인하세요. |
토큰은 사용자를 대신해 움직이며 사용자처럼 메일을 읽을 수 있으므로, 키처럼 서버에 두세요.
인증 코드
도메인 삭제나 웹훅 변경 같은 민감한 변경 전에 API는 액세스 토큰에게, 웹 앱이 사용자에게 요구할 인증 코드를 요구합니다. 호출은 step_up_required?가 true인 403, 즉 OpenEmail::PermissionError를 발생시키며, 아무것도 바뀌지 않았습니다. 코드를 요청하고 사용자가 준 코드를 인증한 뒤 다시 호출하세요. 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이면 사용자가 로그인에 쓰는 주소로 여섯 자리 코드가 가고, sentTo에 가려진 주소가 표시됩니다. totp이면 사용자가 인증 앱의 코드를 읽거나 복구 코드를 씁니다. 아직 열려 있고 시도 횟수가 남은 요청은 resend: true를 전달하지 않는 한 다시 쓰이며, 잠겼거나 만료된 요청은 단순한 호출로 교체됩니다. 각 앱은 사람마다 1시간에 5개, 24시간에 20개까지 열 수 있고, 그다음은 429 step_up_throttled를 발생시킵니다. |
| security.verify_step_up(code:) | 코드를 확인하고 이 앱의 민감한 변경을 60분 동안, elevatedUntil까지, REST와 같은 변경을 하는 MCP 도구 모두에서 풀어 줍니다. 이 앱에서 24시간 동안 틀린 코드가 10개, 또는 사용자의 모든 앱에서 합쳐서 20개가 되면 이 호출과 begin_step_up은 인증이 언제 다시 가능한지 알려 주는 메시지와 함께 429 step_up_locked를 발생시킵니다. |
클라이언트는 스스로 코드를 요구하거나 호출을 반복하지 않으며, 세 메서드 중 어느 것도 자동으로 재시도되지 않습니다. 응답을 잃은 뒤 재시도하면 두 번째 이메일이 가거나 시도 횟수를 한 번 더 쓸 수 있기 때문입니다. 스코프는 필요 없고, API 키로 그중 하나를 호출하면 400 step_up_not_applicable이 돌아옵니다. OpenEmail::STEP_UP_ERROR_CODES는 인증이 실패하는 모든 경우를 담고 있으며, 각각의 대처법은 API 오류 페이지에 있습니다.
업데이트 알림
RubyGems에 gem의 새 버전이 있으면 클라이언트는 프로세스당 한 번, 표준 오류에 ℹ 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 계층을 대체합니다. lambda를 포함해 call(request)에 응답하는 것이면 무엇이든 되며, 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]로 표시되므로, 테스트 로그에 키가 남지 않습니다.
{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}처럼 API의 오류 봉투를 본문으로 하여 2xx 이외의 상태를 반환하면, 그에 맞는OpenEmail::ApiError하위 클래스를 얻습니다.call에서Timeout::Error또는 그 일종인Net::ReadTimeout을 발생시키면timeout?이 true인OpenEmail::NetworkError를 얻습니다.Errno::ECONNREFUSED같은 그 밖의 StandardError는timeout?이 false인NetworkError가 됩니다.- 어댑터 안에서 발생한
NameError,TypeError,ArgumentError는 어댑터의 버그로 간주됩니다. 그대로 발생하며 재시도되지 않습니다.
실패를 연출하는 테스트에서는 max_retries: 0으로 테스트 클라이언트를 만드세요. 그렇지 않으면 반복해도 안전한 호출에서 재시도 가능한 상태나 네트워크 장애가 나면, 사이사이에 실제로 대기하면서 세 번 시도됩니다.