스레드
`threads.list`, `listAll`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze`, `listAttachments`.
읽기
const page = await openemail.threads.list({ folder: 'inbox', query: 'from:ada', labelIds: ['INBOX', 'IMPORTANT'], limit: 25,}) const next = page.nextCursor ? await openemail.threads.list({ folder: 'inbox', cursor: page.nextCursor }) : null const thread = await openemail.threads.get('thread_…')console.log(thread.messageCount, thread.hasUnread, thread.totalReplies)API는 스레드를 pageToken으로 페이지 처리합니다. 클라이언트는 다른 모든 목록과 마찬가지로 이를 nextCursor로 건네주고 cursor로 돌려받으며, listAll과 iterate가 대신 따라갑니다. 이 값은 불투명하므로, 받은 것을 그대로 돌려주고 직접 만들지 마십시오.
정리하기
await openemail.threads.update('thread_…', { read: true, addLabelIds: ['Done'], removeLabelIds: ['INBOX'],}) await openemail.threads.trash('thread_…')await openemail.threads.snooze('thread_…', new Date(Date.now() + 86_400_000))await openemail.threads.unsnooze('thread_…')여기서는 어떤 백엔드에서든 읽음 상태가 곧 레이블이므로, 레이블 목록과 함께 전달되며 두 가지를 동시에 설정해도 순서가 결정적으로 정해집니다. 세 필드 중 최소 하나는 반드시 있어야 합니다.
메시지의 첨부 파일
const files = await openemail.threads.listAttachments('thread_…', 'message_…') for (const file of files) { console.log(file.filename, file.contentType, file.size) if (file.content) await save(file.filename, Buffer.from(file.content, 'base64'))}content는 base64이며, 저장된 바이트를 찾지 못하면 빈 문자열이 되므로 디코딩하기 전에 길이를 확인하십시오. 암호화된 메시지의 암호문은 이 목록에 들어 있고 다른 파일과 똑같이 내려받을 수 있지만, PGP/MIME 버전 파트와 분리된 서명은 그렇지 않습니다. 그것들은 encryption.parts에 id만 남기고 그 이상은 남기지 않습니다.
암호화된 채로 도착한 메시지
이 SDK는 암호화도 복호화도 하지 않습니다. 다른 사람이 암호화한 메시지를 열 수 없고, 암호화된 메시지를 보낼 수도 없습니다. 발송 요청에 암호화 표시가 들어 있으면 거부되는데, 키가 없는 클라이언트가 암호화를 주장할 자격은 없기 때문입니다. OpenEmail 앱에서 생성한 키는 그 키를 만든 브라우저 안에 머물며 여기에는 전혀 닿지 않고, 그 브라우저가 봉인된 메시지를 열더라도 평문은 브라우저 안에 남으며 이 호출이 읽는 저장된 메시지는 여전히 암호문입니다. threads.get이 주는 것은 식별된 봉투입니다. PGP나 S/MIME으로 감싸여 도착한 메시지에는 encryption 객체가 붙으므로 빈 decodedBody만 덜렁 받는 일이 없어지며, encryption은 MessageResource에서 제대로 된 타입을 가진 유일한 필드입니다. 그 부재를 넘겨짚었다가는 감당할 수 없는 유일한 필드이기 때문입니다.
import { isSealed, openemail } from '@openemail/sdk' const thread = await openemail.threads.get('thread_…') for (const message of thread.messages) { if (!message.encryption) continue if (!isSealed(message)) continue console.warn('cannot read this one:', message.encryption.format)}필드의 존재 여부가 아니라 isSealed로 분기하십시오. 다섯 가지 형식 중 pgp-signed와 smime-signed 두 가지는 분리된 서명과 함께 평문으로 도착한 본문을 뜻하므로, 존재 여부로 막으면 숨길 필요가 없던 메일까지 숨겨지고 사용자는 그것을 보지도 설명하지도 못합니다. isSealed가 제공되는 이유가 정확히 이것입니다. 봉인된 형식의 집합은 서버가 한 번 정의하며, 유니온에서 따로 적어 낸 세 번째 사본이야말로 어긋나는 사본입니다.
부재는 평문을 뜻하지 않습니다. encryption은 탐지 기능이 출시되기 전에 저장된 모든 메시지와, 탐지기가 실행되지 않는 경로로 메일함에 들어온 모든 것에서 빠져 있습니다. 이 필드는 메일에 대한 사실이 아니라 우리 탐지 범위에 대한 사실, 즉 아무도 확인하지 않았다는 것을 기록하며, 나중에 채워 넣는 작업도 없습니다.
다른 리소스와 다른 점
ThreadResource.messages의 각 항목은MessageResource이며, 이름이 붙은 필드가 정확히 하나 있는Record<string, unknown>입니다. 나머지에 타입을 붙이는 것은 아무도 수행하지 않는 정규화를 클라이언트가 단언하는 일이 됩니다. 그럼에도encryption에는 이름을 붙였는데, 이 필드로 분기하지 못하는 클라이언트는 봉인된 메시지를 빈 메시지로 읽기 때문입니다.- 충실하게 처리할 수 없는 요청은 그럴듯해 보이지만 조용히 틀린 응답이 아니라 422
capability_unsupported입니다.
파라미터: threads.list (ThreadListOptions)
folderstring- 어느 폴더를 조회할지입니다. 서버 기본값은 `inbox`이므로, 생략하면 전체로 넓어지는 것이 아니라 오히려 좁혀집니다. `query` 검색에도 적용되지만, 쿼리가 `in:`이나 `is:sent` 같은 폴더 `is:`로 폴더를 직접 지정하면 예외입니다.
querystring- 메일함 검색 구문입니다. 따옴표 없는 단어는 모두 나타나야 하며, 각각 느슨하게 일치합니다. 대소문자, 발음 부호, 구분 기호는 무시되고 더 긴 단어의 일부도 일치로 인정되므로 `min`과 `ben jamin` 모두 “Benjamin”을 찾아냅니다. 따옴표로 묶은 구절은 대소문자와 발음 부호를 제외하면 쓰인 그대로 일치하므로 `"ben jamin"`은 “Ben-Jamin”을 찾지 못하며, 검색할 다른 것이 남아 있으면 불용어는 버려집니다. `from:ada`, `label:Invoices`, `is:unread`, `has:pdf`, `before:2026/01/31`, `older_than:1y` 같은 연산자로 범위를 좁히고, `OR`와 괄호, 앞에 붙이는 `-`로 조합하십시오. 검색이 사용할 수 없는 값은 범위를 좁히는 대신 무시됩니다. 단어와 `from:`, `to:`, `cc:`, `subject:`, `body:` 연산자는 가장 최근 메시지의 발신자, 수신자, 제목과 마크업을 제거한 본문 앞 4,000자를 읽는 반면, `filename:`과 `has:`는 대화 전체의 모든 첨부 파일을 읽고 레이블과 폴더는 대화 전체를 읽습니다. 필터를 걸지 않은 목록이 읽는 것과 같은 색인을 좁힐 뿐입니다. 봉인된 메시지는 본문 텍스트를 저장하지 않으므로 발신자, 수신자, 제목만 일치할 수 있습니다.
labelIdsstring | string[]- 지정한 레이블이 붙은 스레드로 목록을 제한합니다. 엔드포인트는 쉼표로 구분된 문자열을 받고, 클라이언트가 배열을 하나의 문자열로 이어 줍니다. 몇 개까지 지정할 수 있는지에 대한 제한은 없습니다.
limitnumber- 반환할 스레드 수이며 1에서 100까지입니다. 생략하면 핸들러가 25를 사용합니다. 기본값이 스키마가 아니라 핸들러에 있으므로, 값을 생략하는 것과 25를 명시하는 것이 똑같이 동작합니다.
cursorstring- 이전 페이지의 `nextCursor`를 그대로 다시 보냅니다. 다른 모든 목록이 쓰는 이름으로 감싼 API의 `pageToken`이며 불투명하므로, 직접 만들거나 수정하지 마십시오.
응답: Page<ThreadSummaryResource>
itemsThreadSummaryResource[]- 이 페이지에 담긴 스레드마다 하나씩이며, API의 `data` 봉투에서 꺼낸 것입니다. 각 항목에는 객체 표시자와 id밖에 없습니다. 목록에는 제목, 스니펫, 참여자, 레이블이 전혀 담기지 않으므로, 그 이상이 필요하면 원하는 스레드에 대해 `threads.get`을 호출해야 합니다.
items[].idstring- 스레드의 id이며, `threads.get`, `threads.update` 등에 그대로 넘기면 됩니다. 필터를 건 목록에서 나온 행이든 `query` 검색에서 나온 행이든 같은 id입니다.
hasMoreboolean- 다음 페이지가 있는지 여부이며, API가 명시하지 않는 경우 `nextCursor`에서 파생합니다.
nextCursorstring | null- 다음 페이지를 위해 `cursor`로 돌려보낼 API의 `nextPageToken`이며, 다음 페이지가 없으면 null입니다. 빈 토큰은 null로 정규화되므로 falsy 검사와 null 검사의 결과가 일치합니다.