구성
클라이언트를 만드는 세 가지 방법, 모든 옵션, 그리고 요청을 보내기 전에 거부하는 것들.
옵션
import os from openemail import AsyncOpenEmail, OpenEmail, init, openemail init(os.environ['OPENEMAIL_API_KEY'])openemail.me.ping() billing = OpenEmail(os.environ['BILLING_API_KEY']) pinned = OpenEmail('oe_live_...', base_url='https://api.openemail.uk') background = AsyncOpenEmail()| 진입점 | 제공하는 것 |
|---|---|
| init(...) | 공유 클라이언트를 구성하고 반환합니다. 그때부터 모든 모듈에서 openemail이 그 클라이언트이며, 생략한 값은 환경 변수에서 읽습니다. |
| openemail | 공유 클라이언트입니다. init 전에 사용하면 첫 호출 시 OPENEMAIL_API_KEY와 OPENEMAIL_BASE_URL로 스스로를 구성합니다. |
| OpenEmail(...) | 별도의 클라이언트입니다. 공유 클라이언트와 별개로 두 번째 키를 쓰거나, 직접 만든 모듈이 export할 인스턴스를 구성할 때 씁니다. 생략한 값은 init처럼 환경 변수에서 읽으며, create_client는 같은 클래스의 다른 이름입니다. |
| AsyncOpenEmail(...) | 같은 옵션을 받고 모든 메서드를 await로 호출하는 별도의 비동기 클라이언트입니다. 공유 openemail은 동기 클라이언트이므로 이것은 직접 만들어야 합니다. |
| get_client()과 reset_client() | get_client는 공유 클라이언트 자체를 반환하며, init이 실행되지 않았다면 환경 변수로 그 클라이언트를 만듭니다. reset_client는 그 클라이언트를 버리므로, 다음에 사용할 때 새 클라이언트가 만들어집니다. |
import httpxfrom openemail import init init( 'oe_live_...', base_url='https://api.openemail.uk', timeout=30, max_retries=2, http_client=httpx.Client(proxy='http://proxy.internal:3128'), headers={'X-Team': 'billing'}, user_agent='billing-service/1.4', disable_update_notice=True,)| 옵션 | 기본값 | 설명 |
|---|---|---|
| api_key | OPENEMAIL_API_KEY | init과 OpenEmail이 환경 변수에서 읽습니다. oe_live_ 또는 oe_test_로 시작해야 합니다. |
| access_token | OPENEMAIL_ACCESS_TOKEN | api_key 대신 쓰는 OAuth 액세스 토큰, 또는 그 토큰을 반환하는 함수입니다. 아래의 OAuth 액세스 토큰 절을 참고하세요. |
| base_url | https://api.openemail.uk | 또는 OPENEMAIL_BASE_URL. 끝의 슬래시는 제거되며, init과 OpenEmail은 호스트만 적힌 값 앞에 https://를, 이 컴퓨터의 호스트(localhost, 127.x.x.x 주소, ::1) 앞에는 http://를 붙입니다. 자격 증명은 그 밖의 호스트로 암호화되지 않은 http를 통해 보내지지 않으며, 0.0.0.0이나 [::]는 클라이언트를 만들 때 예외를 발생시킵니다. 이 주소들은 서버가 수신 대기하는 주소이지, 요청을 보낼 주소가 아니기 때문입니다. |
| timeout | 30 | 초 단위이며, 호출 단위가 아니라 시도 단위입니다. 헤더뿐 아니라 본문을 읽는 시간까지 포함합니다. 0이면 비활성화됩니다. files.upload는 호출에서 별도의 timeout을 넘기지 않는 한 최소 600초를 기다립니다. |
| max_retries | 2 | 반복해도 안전한 호출에 한해, 첫 시도 이후의 추가 시도 횟수입니다. 호출별이 아니라 클라이언트에 설정합니다. |
| http_client | 새 httpx.Client | 프록시, 직접 정한 TLS 설정, 마운트한 전송 계층, 테스트 더블을 쓰려면 직접 만든 것을 전달하세요. OpenEmail에는 httpx.Client를, AsyncOpenEmail에는 httpx.AsyncClient를 넘깁니다. 클라이언트를 닫아도 직접 넘긴 것은 열린 채로 남습니다. |
| headers | {} | 모든 요청에 전송됩니다. |
| user_agent | openemail-python/<version> | 모든 요청에 전송됩니다. |
| disable_update_notice | False | PyPI에 새 버전이 있는지 프로세스당 한 번 확인하는 절차를 건너뜁니다. 이 확인은 출력이 터미널로 갈 때만 실행되며, OPENEMAIL_DISABLE_UPDATE_NOTICE로도 끌 수 있습니다. |
보내기 전에 거부하는 것
이들은 첫 발송에서 알 수 없는 실패로 나타나지 않고, 요청이 전송되기 전에 ValueError를, 표에 그렇게 적힌 경우에는 TypeError를 발생시킵니다. 메시지가 무엇이 잘못되었고 대신 무엇을 넘겨야 하는지 알려 줍니다.
| 거부되는 것 | 이유 |
|---|---|
| 키가 전혀 없음 | api_key도 OPENEMAIL_API_KEY도 설정되지 않아 인증할 수단이 없습니다. |
| 세션 쿠키 또는 세션 토큰 | 여기서 인증되는 것은 oe_live_와 oe_test_뿐이며, API도 같은 말을 합니다. 검사는 접두사 확인 그 이상이 아니므로, 폐기된 키는 여전히 통신 단계에서 실패합니다. |
| http 또는 https URL이 아닌 base_url | 그 외의 것은 가져올 수 없으므로, 클라이언트는 첫 요청에서 실패하는 대신 만들어질 때 이를 거부합니다. |
| 0.0.0.0이나 [::]를 가리키는 base_url | 서버가 수신 대기하는 주소이지, 요청을 보낼 주소가 아닙니다. 메시지는 대신 같은 포트의 127.0.0.1이나 [::1]을 제시합니다. |
| 암호화되지 않은 http로 보내는 자격 증명 | 서버가 이 컴퓨터에 있지 않은 한, 요청이 나가기 전에 호출 시점에서 거부됩니다. 네트워크상의 누구나 그것을 읽을 수 있기 때문입니다. |
| 메서드에 빈 id나 점으로만 이루어진 id를 전달 | 메서드를 호출할 때 발생합니다. 점으로 이루어진 경로 세그먼트는 모든 URL 파서가 제거하므로, 요청이 다른 엔드포인트에 도달하게 됩니다. |
| 종류가 잘못된 http_client | 클라이언트를 만들 때 TypeError가 발생합니다. OpenEmail은 httpx.Client를, AsyncOpenEmail은 httpx.AsyncClient를 받습니다. |
| JSON으로 담을 수 없는 본문 값 | 그 타입을 지목하는 TypeError가 발생합니다. JSON 타입은 그대로 통과하며, datetime, date, set은 자동으로 변환됩니다. |
test_mode 옵션은 없고 앞으로도 없을 것입니다. 키 체계는 힌트가 아니라 자격 증명의 일부이므로, 모드는 키의 속성입니다. client.mode는 접두사를 읽을 뿐 아무것도 결정하지 않습니다.
하나의 클라이언트, 여러 개의 키
클라이언트는 한 번 만들어 공유하세요. 요청마다 새 인스턴스를 만들면 연결 풀과 설정을 헛되이 버리는 셈이고, 인스턴스에 담긴 상태 중 호출자별로 달라지는 것은 없습니다.
클라이언트 하나를 여러 스레드가 안전하게 공유할 수 있습니다. close()를 호출하거나 with 블록이 끝나면 클라이언트가 연 연결 풀이 닫히며, 직접 넘긴 http_client는 열린 채로 남으므로 직접 닫아야 합니다.
여러 워크스페이스를 대신해 발송하는 작업처럼 키마다 인스턴스를 만들어야 할 상황에서는 호출에 api_key를 전달하세요. 그 요청에 한해 Authorization 헤더를 대체하며 클라이언트에는 아무것도 남기지 않습니다.
from openemail.types import EmailSend workspace_key = 'oe_live_...' message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} 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 밖의 모든 메서드가 timeout과 나란히 키워드 인자로 이 값을 받습니다. 이 값은 생성자와 같은 규칙으로 요청 전에 검사되므로, 오타가 나면 나중에 찾아 헤매야 할 자격 증명에 대한 401이 아니라 api_key= on this call을 지목하는 ValueError가 발생합니다. 재시도된 호출은 받은 키를 그대로 유지합니다.
timeout은 초 단위이며, 그 호출 하나에 한해 각 시도마다 클라이언트의 타임아웃을 대체합니다.
client.mode는 클라이언트를 생성할 때 사용한 키를 설명하며, 호출별 재정의를 따라가지 않습니다. 하나의 클라이언트가 여러 키를 쓰는 순간 보고할 단일 모드가 없어지므로, 전달한 키에서 직접 읽으세요.
메서드가 감싸지 않은 엔드포인트
client.raw.request는 클라이언트의 자격 증명, 기본 URL, 타임아웃, 재시도 정책을 적용해 요청을 보내고 파싱된 JSON을 반환합니다. method, query, body, api_key, timeout을 받습니다. repeatable=True를 넘기지 않는 한 GET은 재시도하고 그 밖의 요청은 한 번만 보냅니다. idempotent=True를 주면 Idempotency-Key가 붙으며, 키는 idempotency_key로 넘긴 값이거나 새로 만든 값입니다.
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})경로는 / 하나로 시작해야 합니다. //host/x처럼 그렇지 않은 경로는 요청을 보내기 전에 예외를 발생시키며, 완성된 URL이 기본 URL의 오리진을 벗어나는 경로도 마찬가지이므로, 요청에 담긴 자격 증명이 다른 호스트에 닿는 일은 없습니다.
일회용 받은편지함
create_temp_mail()은 API 키를 담지 않는 클라이언트를 만들며, create_async_temp_mail()은 그 비동기 버전입니다. 익명으로 받은편지함을 만들고, 읽을 때마다 create가 반환한 받은편지함 토큰이나 extend가 반환한 더 새로운 토큰을 호출별 inbox_token으로 보내거나 create_temp_mail(inbox_token=...)으로 한 번에 지정합니다.
from openemail import create_temp_mail temp_mail = create_temp_mail() inbox = temp_mail.create()messages = temp_mail.list_messages(inbox['id'], inbox_token=inbox['token']) print(messages['items'], messages['expiresAt'])OAuth 액세스 토큰
아직 출시되지 않음명령줄 도구나 에이전트처럼 사용자가 OAuth로 연결한 앱은 API 키 대신 액세스 토큰을 가집니다. 이를 access_token으로 넘기세요. 토큰 자체이거나 토큰을 반환하는 함수이며, AsyncOpenEmail에서는 이 함수가 async여도 됩니다. 함수는 호출마다 한 번 불리고, 그 호출의 재시도는 함수가 반환한 값을 다시 씁니다. 그러니 만료가 가까워지면 함수 안에서 토큰을 갱신하세요. 클라이언트를 다시 만들 필요가 없습니다.
from openemail import OpenEmail client = OpenEmail(access_token=session.fresh_access_token) me = client.me.get() if me['object'] == 'oauth_token': print(me['clientId'], me['expiresAt'])| 경우 | 일어나는 일 |
|---|---|
| api_key와 access_token을 함께 주거나 둘 다 주지 않음 | 생성자가 ValueError를 발생시킵니다. 둘 다 없으면 메시지에 OPENEMAIL_API_KEY와 OPENEMAIL_ACCESS_TOKEN이 나옵니다. |
| 토큰이 아닌 값 | 토큰은 1~512자이고 oe_로 시작하지 않습니다. is_access_token이 하는 검사입니다. 이 검사를 통과하지 못한 문자열은 생성자에서 예외를 발생시키고, 그런 값을 반환하는 함수는 아무것도 보내기 전에 호출을 실패시킵니다. |
| OPENEMAIL_ACCESS_TOKEN | 두 자격 증명 중 어느 것도 넘기지 않고 OPENEMAIL_API_KEY도 설정되지 않았을 때 init, OpenEmail, 공유 openemail이 읽습니다. 즉 환경의 키가 우선합니다. |
| 예외를 발생시키는 함수 | 호출은 그 예외를 바꾸지 않고 그대로 발생시키며, 아무것도 보내지 않습니다. |
| OpenEmail에 넘긴, awaitable을 반환하는 함수 | 동기 클라이언트는 그것을 기다릴 수 없으므로 ValueError가 발생합니다. AsyncOpenEmail에서는 함수가 async여도 됩니다. |
| 호출별 api_key | 그 요청 하나에 한해 토큰을 대체하고, 함수는 호출되지 않습니다. |
| mode | 토큰에서는 항상 live입니다. |
| create_temp_mail() | 환경에 무엇이 있든 자격 증명을 보내지 않습니다. |
| me.get()과 me.ping() | 토큰일 때 get은 object가 oauth_token, id와 roleId가 None, 연결된 앱의 clientId, 그리고 사용자가 앱에 준 승인이 끝나는 시점인 expiresAt으로 응답합니다. ping은 kind가 oauth, keyId가 None, 그리고 clientId로 응답합니다. KeyResource와 PingResource는 유니언 타입이므로, clientId나 expiresAt을 읽기 전에 object나 kind로 구분하세요. |
토큰은 사용자를 대신해 움직이며 사용자처럼 메일을 읽을 수 있으므로, 키처럼 서버에 두세요.
인증 코드
아직 출시되지 않음도메인 삭제나 웹훅 변경 같은 민감한 변경 전에 API는 액세스 토큰에게, 웹 앱이 사용자에게 요구할 인증 코드를 요구합니다. 호출은 is_step_up_required가 True인 OpenEmailApiError를 발생시키며, 아무것도 바뀌지 않았습니다. 코드를 요청하고 사용자가 준 코드를 인증한 뒤 다시 호출하세요. API 키는 요구받지 않습니다.
from openemail import OpenEmailApiError try: client.domains.delete(domain_id)except OpenEmailApiError as error: if not error.is_step_up_required: raise challenge = client.security.begin_step_up() if challenge['method'] == 'email': prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: ' else: prompt = 'Enter the code from your authenticator app, or a backup code: ' client.security.verify_step_up({'code': input(prompt)}) client.domains.delete(domain_id)| 메서드 | 하는 일 |
|---|---|
| security.step_up_status() | 앱이 지금 인증된 상태인지(elevated, elevatedUntil), 다음 코드를 어떻게 확인하는지(method, email 또는 totp), 그리고 유효 시간의 길이인 minutes. 아무것도 보내지 않으며 일시 중지를 알려 주지 않습니다. |
| security.begin_step_up(body=None) | 인증 요청을 엽니다. email이면 사용자가 로그인에 쓰는 주소로 여섯 자리 코드가 가고, sentTo에 가려진 주소가 표시됩니다. totp이면 사용자가 인증 앱의 코드를 읽거나 복구 코드를 씁니다. 아직 열려 있고 시도 횟수가 남은 요청은 {'resend': True}를 넘기지 않는 한 다시 쓰이며, 잠겼거나 만료된 요청은 단순한 호출로 교체됩니다. 각 앱은 사람마다 1시간에 5개, 24시간에 20개까지 열 수 있고, 그다음은 429 step_up_throttled를 발생시킵니다. |
| security.verify_step_up({'code': code}) | 코드를 확인하고 이 앱의 민감한 변경을 60분 동안, elevatedUntil까지, REST와 같은 변경을 하는 MCP 도구 모두에서 풀어 줍니다. 이 앱에서 24시간 동안 틀린 코드가 10개, 또는 사용자의 모든 앱에서 합쳐서 20개가 되면 이 호출과 begin_step_up은 인증이 언제 다시 가능한지 알려 주는 메시지와 함께 429 step_up_locked를 발생시킵니다. |
클라이언트는 스스로 코드를 요구하거나 호출을 반복하지 않으며, begin_step_up과 verify_step_up 모두 자동으로 재시도되지 않습니다. 응답을 잃은 뒤 재시도하면 두 번째 이메일이 가거나 시도 횟수를 한 번 더 쓸 수 있기 때문입니다. 스코프는 필요 없고, API 키로 둘 중 하나를 호출하면 400 step_up_not_applicable이 돌아옵니다. STEP_UP_ERROR_CODES는 인증이 실패하는 모든 경우를 담고 있으며, 각각의 대처법은 API 오류 페이지에 있습니다.