스레드
메일을 읽고 정리합니다.
이 페이지의 7개 호출을 본인 키로 워크스페이스에 실제로 실행합니다.
목록 조회
GET /threads?folder=inbox. query를 전달하면 같은 로컬 인덱스를 검색합니다. 일반 단어는 모두 나타나야 하고 각각은 대소문자, 발음 구별 부호, 구분자를 무시하고 느슨하게 일치하므로 min으로 "Benjamin"을 찾습니다. 따옴표로 묶은 구절은 대소문자와 발음 구별 부호를 제외하면 적힌 그대로 일치하므로 "ben jamin"은 "Ben-Jamin"을 찾지 못합니다. the나 emails 같은 불용어는 검색할 다른 것이 남아 있을 때 일반 단어 목록에서 제외됩니다. from:, to:, subject:, label:, is:unread, has:pdf, after:2026/01/31, newer_than:7d 같은 연산자가 범위를 좁히고, OR와 괄호, 앞에 붙인 -가 이들을 조합합니다. 수신자는 역할 구분 없이 하나의 목록으로 저장되고 Bcc는 결코 담기지 않으므로, cc:는 to:와 같은 필드를 읽고 bcc:는 자체적으로 아무것도 일치시키지 않습니다. from:me는 보낸 메일이고, to:me는 별칭을 포함한 자신의 주소 중 하나를 수신자로 담고 있거나 그 주소로 배달된 메일입니다.
단어와 from:, to:, cc:, subject:, body: 연산자는 각 스레드의 가장 최신 메시지, 즉 그 발신자, 수신자, 제목, 본문 앞부분 4,000자를 읽습니다. filename:과 has:는 대화 전체의 모든 첨부 파일을 읽고, label:, in:, is:는 대화 전체를 읽습니다. folder는 쿼리가 in:으로, 또는 is:sent처럼 폴더를 가리키는 is:로 폴더를 지정하지 않는 한 계속 적용되며, in:anywhere는 단독으로든 다른 항목과 함께든 모든 폴더를 검색합니다. 초안 목록 조회는 예외로, 쿼리가 무엇을 지정하든 초안에 머뭅니다.
검색이 사용할 수 없는 값은 범위를 좁히는 대신 무시되므로, 값에 오타가 나면 결과가 비는 대신 넓어집니다. category:, larger:, smaller:, size:, messagesize:, list:, rfc822msgid:, received:, sent:, is:promotions 같은 카테고리 단어, 어떤 첨부 종류도 가리키지 않는 has: 단어, high나 low가 아닌 importance:, 읽을 수 없는 날짜, 단위가 h, d, w, m, y가 아닌 기간이 여기에 해당합니다. 알지 못하는 연산자 이름, 예를 들어 project:는 일반 텍스트로 검색됩니다. 날짜는 스레드의 가장 최근 활동을 UTC 기준으로 읽으며, after:는 지정한 날을 포함하고 before:는 제외합니다. 날짜는 YYYY/MM/DD, YYYY-MM-DD, YYYYMMDD, 연도만, 또는 epoch 초나 밀리초로 적습니다.
nextPageToken은 불투명한 값입니다. 받은 것을 그대로 돌려보내고, 직접 만들거나 수정하지 마세요. 그 형태는 계약의 일부가 아닙니다.
단건 조회
GET /threads/{id}는 가장 최신 메시지뿐 아니라 스레드의 모든 메시지를 레이블, 그리고 안에 읽지 않은 것이 있는지 여부와 함께 반환합니다.
암호화된 채로 도착한 메시지
이 API는 암호화도 복호화도 하지 않습니다. 다른 사람이 암호화한 메시지를 열 수 없고, 암호화된 메시지를 보낼 수도 없습니다. 암호화 표시를 담은 요청은 422로 거부되는데, 그것을 설정해도 되는 표면은 키를 가진 쪽뿐이고 어떤 API 클라이언트도 키를 가지고 있지 않기 때문입니다. 이 API가 하는 일은 들어오는 메일에서 봉인된 봉투를 최상위 Content-Type만 보고 인식한 다음, 그 사실을 메시지에 표시하는 것입니다.
이제 OpenEmail 자체도 키를 보유하므로, 어느 쪽 키를 어디에 두는지 정확히 짚어 둘 필요가 있습니다. 메일함 소유자는 브라우저에서 OpenPGP 신원을 생성하고 공개 키를 디렉터리에 게시하여, 로그인한 다른 OpenEmail 발신자가 조회할 수 있게 합니다. 비밀 키는 그 브라우저에서 만들어지고 이곳으로 전송되지 않으며 복구할 수도 없으므로, 이 API의 어떤 것도 무엇인가를 복호화할 수 없고 어떤 지원 요청이나 소환장, 우리 쪽 백업으로도 키를 얻을 수 없습니다. 웹 앱은 이제 읽는 사람의 브라우저에 키가 있을 때 PGP/MIME 또는 인라인 PGP 메시지를 열 수 있지만, 그 복호화는 탭 안에서 일어나고 평문은 결코 되저장되지 않습니다. 저장된 메시지는 암호문 그대로 남고, 이 API의 어떤 응답도 열린 텍스트를 싣지 않습니다. 또한 앱은 이제 브라우저에서 새 메시지를 봉인해 보낼 수 있습니다. 작성기가 수신자의 게시된 키로 암호화하고 메일은 PGP/MIME으로 나갑니다. 이 API는 여전히 아무것도 봉인할 수 없으므로, 아래 필드는 다른 사람이 암호화한 메일과 OpenEmail 탭에서 봉인된 메일 모두를 설명합니다.
이것이 필드로 존재할 가치가 있는 이유는 그 대안 때문입니다. 봉인된 메시지는 읽을 수 있는 본문을 저장하지 않으므로 decodedBody가 ""로 돌아오는데, 이는 실제로 내용이 없던 메시지와 똑같은 바이트입니다. encryption은 무언가를 처리하기 전에 그 둘을 구별할 수 있게 해 주며, 검증이 아니라 봉투에 관한 진술입니다. 메시지가 봉인되어 있음을 아는 것과 그것을 열어 본 것은 같지 않습니다.
{ "object": "thread", "id": "thread_2f9b…", "messages": [ { "id": "msg_7c41…", "subject": "Q3 numbers", "decodedBody": "", "encryption": { "format": "pgp-mime", "detectedAt": "2026-08-30T09:14:22.117Z", "rawRetained": false, "parts": [ { "index": 0, "attachmentId": "msg_7c41…-0", "role": "version" }, { "index": 1, "attachmentId": "msg_7c41…-1", "role": "ciphertext" } ] } } ] }encryption
format'pgp-mime' | 'pgp-signed' | 'pgp-inline' | 'smime-encrypted' | 'smime-signed'- 어떤 봉투가 도착했는지입니다. 최상위 `Content-Type`(PGP는 그 `protocol` 매개변수, S/MIME은 `smime-type`)에서 읽어 내거나, `pgp-inline`의 경우 PGP armor 헤더로 시작하는 본문에서 읽어 냅니다. `smime-type`이 전혀 없는 `pkcs7-mime` 부분은 `smime-encrypted`로 읽는데, RFC 8551이 이를 기본값으로 정하고 있기 때문입니다.
detectedAtstring- ISO 8601 형식으로, 탐지기가 실행된 시각이며 이는 메시지가 여기에 수집된 시각입니다. 메시지가 언제 누구에 의해 암호화되었는지는 말해 주지 않습니다.
rawRetainedboolean- 원본 RFC822 바이트가 보관되어 메시지를 통째로 돌려줄 수 있는지 여부입니다. 현재는 모든 메시지에서 false인데, 아직 여기서 원본 메일을 보관하지 않기 때문입니다. 이 값이 지금 응답에 들어 있는 이유는, 그것이 바뀌는 날이 저장된 모든 메시지를 다시 마이그레이션해야 하는 날이 되지 않게 하기 위해서입니다.
partsobject[]- 이 형식이 사용하는 봉투 부분들입니다. `encryption`이 있으면 항상 존재하며, 가리킬 것이 없으면 비어 있습니다. `pgp-inline`은 별도의 부분이 아예 없는데, armor 자체가 본문이고 `decodedBody`로 도착하기 때문입니다.
parts[].indexnumber- 원본 메시지의 몇 번째 MIME 부분이었는지로, `attachments`가 아니라 도착한 그대로의 부분들을 기준으로 셉니다. 두 목록은 서로 다르며, 이 값을 기록하는 이유가 바로 그것입니다.
parts[].attachmentIdstring- 이 부분이 `attachments`에서 가지는 id이며, 애초에 거기에 나타나는 경우에 한합니다. 메시지 id 뒤에 부분 인덱스를 붙인 값입니다. `ciphertext` 부분은 목록에 나오고 다른 파일처럼 내려받을 수 있지만, `version`과 `signature`는 목록에서 빠지므로 그 id는 두 관점을 대조하는 용도일 뿐입니다. 첨부 파일 엔드포인트는 그것들을 반환하지 않습니다.
parts[].role'version' | 'ciphertext' | 'signature'- `version`은 PGP/MIME 제어 부분, `ciphertext`는 메시지 본체, `signature`는 분리형 서명입니다. 가져올 가치가 있는 것은 `ciphertext`뿐이며, 나머지 둘은 예전에 쓰레기 첨부 파일처럼 표시되던 프로토콜 부속물이고 이제는 그렇지 않습니다.
| format | 도착한 것 | 본문 |
|---|---|---|
| pgp-mime | PGP/MIME 봉투입니다. protocol=application/pgp-encrypted가 붙은 multipart/encrypted. | 봉인됨 |
| pgp-inline | 본문 자체에 들어 있는 armor입니다. 오직 본문 텍스트에서만 읽어 내므로, armor 블록을 인용하기만 한 답장이 이것으로 오인되지 않습니다. | 봉인됨 |
| smime-encrypted | smime-type=enveloped-data가 붙은 S/MIME pkcs7-mime 부분이거나, smime-type이 전혀 없는 부분입니다. | 봉인됨 |
| pgp-signed | 메시지 옆에 붙은 분리형 PGP 서명입니다. protocol=application/pgp-signature가 붙은 multipart/signed. | 읽을 수 있음 |
| smime-signed | 분리형 S/MIME 서명입니다. pkcs7-signature 프로토콜이거나 smime-type=signed-data. | 읽을 수 있음 |
서명은 봉인이 아니며, format이 아니라 encryption의 존재 여부로 분기하면 정확히 반대로 처리하게 됩니다. 서명은 메시지를 감싼 포장이 아니라 누가 썼는지에 대한 주장입니다. 서명된 메시지의 본문은 평문이고 다른 메일과 똑같이 읽힙니다. pgp-mime, pgp-inline, smime-encrypted는 읽을 수 없는 것으로, 서명된 두 형식은 평범한 메일로 다루세요.
봉인된 메시지에서 달라지는 것
달라지는 것은 봉인된 세 형식뿐이며, 그 변화는 이 응답이 아니라 수집 시점에 일어납니다. 본문을 읽었을 모든 처리는 암호문을 읽고 얻을 수 없었던 결과를 보고하는 대신 물러납니다.
- 본문 검색. 메시지는 빈 본문 스니펫으로 색인되므로 발신자, 제목, 주소, 레이블로는 여전히 찾을 수 있고 내용으로는 찾을 수 없습니다.
- 피싱 점수 계산기의 본문 검사. 판정은 여전히 도착하며 무엇을 하지 못했는지 밝힙니다.
risk.signals에body-encrypted가 실리고risk.aiChecked는 false입니다. - AI 작성 여부 검사. 추측하는 대신 판단을 거부합니다.
aiWritten.level은unknown이고aiWritten.skipped는encrypted입니다. - 규칙의 본문 조건. 봉투와 헤더 조건은 이전과 똑같이 동작하지만, 본문을 묻는 규칙은 불일치로 집계되지 않고 평가되지 않은 것으로 기록됩니다. "일치하지 않았다"와 "읽을 수 없었다"는 다른 답이기 때문입니다.
- 캘린더 초대 가져오기. 초대는 암호문 안에 있으며, 봉투만 보고 일정을 만들면 실제 캘린더에 잘못된 항목이 올라갑니다.
- 스레드 요약과 임베딩. 대화 전체에 적용됩니다. 봉인된 답장 하나면 충분합니다. 요약은 모델이 평문을 읽어 낸 결과를 평문 메타데이터로 저장하는 것이고, 이 파이프라인에서 본문이 아무도 본문이라고 생각하지 않는 저장소로 새어 나갈 수 있는 유일한 지점입니다.
본문이 필요하지 않은 것은 모두 그대로입니다.
- DMARC, DKIM, SPF. 이들은
Authentication-Results에서 읽는데 암호문이 그것을 가리지 않으므로, 암호화된 메시지도 판정이 없는 대신 실제 인증 판정을 받습니다. - 스레딩, 스팸 분류, 차단 목록. 모두 봉투와 헤더를 다루는 작업입니다.
- 첨부 파일. 암호문 부분은
attachments에 남아 있고, 이름 없이 도착하면encrypted-message.asc로 명명되며, 아래 엔드포인트로 내려받습니다. 웹 앱의 리더가 브라우저에서 가져와 복호화하는 것이 정확히 이 파일이며, 키가 없는 API 클라이언트에게는 그 다운로드가 이 메일을 읽는 유일한 방법입니다. 키를 가진 클라이언트에서 여세요. - 서명된 메시지는 이 중 무엇도 잃지 않습니다. 위의 모든 검사가 그대로 실행되고 보류되는 것이 없으며, 봉인된 목록이 다섯이 아니라 세 형식인 이유가 그것입니다.
encryption이 없다는 것은 평문이라는 주장이 아닙니다. 아무도 확인하지 않았다는 뜻입니다. 메시지가 탐지 기능보다 앞서 있거나, 탐지기를 거치지 않는 경로로 메일함에 들어온 것입니다. 소급 적용도 없으므로, "확인하지 않았다"고 말하는 필드를 "확인했고 암호화가 없었다"로 읽어서는 절대 안 됩니다.
표시와 레이블
PATCH /threads/{id}는 read, addLabelIds, removeLabelIds를 받습니다. 읽음 상태는 이 제품이 지원하는 모든 백엔드에서 레이블이므로, read 설정과 레이블 이동을 한 호출로 처리하면 순서가 결정적으로 유지됩니다.
{ "read": true, "addLabelIds": ["USER_INVOICES"] }TRASH와 SNOOZED는 여기서 label_not_directly_settable로 거부됩니다. 두 상태 모두 레이블만으로 표현되지 않으므로(휴지통으로 옮기면 폴더 레이블도 지워지고, 스누즈에는 함께 저장되는 깨우기 시각이 필요합니다), 손으로 설정하면 앱이 결코 만들지 않고 되돌릴 수도 없는 상태에 스레드가 남습니다. 아래 엔드포인트를 쓰세요.
휴지통과 스누즈
| 엔드포인트 | 동작 |
|---|---|
| POST /threads/{id}/trash | Bin으로 옮기면서 INBOX, SPAM, SNOOZED, ARCHIVE를 함께 지웁니다. |
| POST /threads/{id}/snooze | 본문은 { "wakeAt": "…" }입니다. 스레드를 숨기고 돌아올 시각을 예약합니다. |
| POST /threads/{id}/unsnooze | 지금 바로 되돌리고, 예약된 복귀를 취소합니다. |
스누즈는 두 가지를 기록합니다. 스레드를 숨기는 레이블과, 스레드를 되돌리는 항목입니다. 둘 중 하나만 하는 상황이야말로 이것이 레이블 편집이 아니라 별도의 엔드포인트인 이유입니다.
첨부 파일
GET /threads/{id}/messages/{messageId}/attachments는 각 첨부 파일을 filename, contentType, size, 그리고 base64로 인코딩된 content와 함께 반환합니다. 저장된 바이트를 찾지 못한 경우 content는 빈 문자열이므로, 디코딩하기 전에 길이를 확인하세요.
암호화된 봉투가 전부 여기 있는 것은 아닙니다. 암호문은 있지만(그것이 메시지이고, 내려받는 것이 API 클라이언트가 이 메일을 읽는 유일한 방법입니다) PGP/MIME의 version 부분과 분리형 서명은 목록에서 제외됩니다. 쓰레기 첨부 파일처럼 표시되었고 호출자가 그것으로 할 수 있는 일이 없기 때문입니다. 둘 다 encryption.parts에 id가 남아 두 관점을 대조할 수 있지만, 이 엔드포인트는 그것들을 반환하지 않습니다.