스레드
`threads.list`, `list_all`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze`, `list_attachments`.
읽기
from openemail import openemail page = openemail.threads.list( folder='inbox', query='from:ada', label_ids=['INBOX', 'IMPORTANT'], limit=25,) next_page = ( openemail.threads.list(folder='inbox', cursor=page['nextCursor']) if page['nextCursor'] else None) thread = openemail.threads.get('thread_…')print(thread['messageCount'], thread['hasUnread'], thread['totalReplies'])API는 스레드를 pageToken으로 페이지 처리합니다. 클라이언트는 다른 모든 목록과 마찬가지로 이를 nextCursor로 건네주고 cursor로 돌려받으며, list_all과 iterate가 대신 따라갑니다. 이 값은 불투명하므로, 받은 것을 그대로 돌려주고 직접 만들지 마십시오.
목록 필터는 snake_case 키워드 인자(label_ids=, date_from=)이지만, 요청 본문의 키는 API의 camelCase 이름(update의 addLabelIds)을 그대로 씁니다. 페이지와 스레드는 dict로 돌아오므로 page['nextCursor']와 thread['messageCount']로 읽습니다.
from datetime import datetime, timedelta, timezone from openemail import openemail now = datetime.now(timezone.utc) last_week = openemail.threads.list_all( sort='oldest', date_from=now - timedelta(days=7), date_to=now, from_contacts=True,) for thread in openemail.threads.iterate(sort='sender'): print(thread['id'])sort, date_from, date_to, from_contacts는 스레드 목록 자체의 조작입니다. sort는 newest, oldest, sender, subject 중 하나이고, 날짜는 datetime이나 ISO 8601 문자열을 받으며 양 끝을 모두 포함하고, from_contacts는 최신 메시지가 저장된 연락처에서 온 메일만 남깁니다. 어느 정렬이든 스레드를 건너뛰거나 반복하지 않고 끝까지 페이지를 넘깁니다. tzinfo가 없는 datetime은 현지 시각으로 해석됩니다.
정리
from datetime import datetime, timedelta, timezone from openemail import openemail openemail.threads.update('thread_…', { 'read': True, 'addLabelIds': ['USER_DONE'], 'removeLabelIds': ['INBOX'],}) openemail.threads.trash('thread_…')openemail.threads.snooze('thread_…', datetime.now(timezone.utc) + timedelta(days=1))openemail.threads.unsnooze('thread_…')여기서는 어떤 백엔드에서든 읽음 상태가 곧 레이블이므로, 레이블 목록과 함께 전달되며 두 가지를 동시에 설정해도 순서가 결정적으로 정해집니다. 세 필드 중 최소 하나는 반드시 있어야 합니다.
addLabelIds는 labels.list에서 얻은 ID와 ARCHIVE, STARRED 같은 시스템 ID를 받습니다. 어떤 라벨도 가리키지 않는 ID는 만들어지지 않고 422 label_not_found로 거부되므로, 먼저 labels.create로 라벨을 만드세요. threads.list(folder='USER_DONE')는 어느 폴더에 있든 그 라벨이 붙은 모든 스레드를 나열합니다.
메시지의 첨부 파일
import base64from pathlib import Path from openemail import openemail files = openemail.threads.list_attachments('thread_…', 'message_…') for file in files: print(file['filename'], file['contentType'], file['size']) if file['content']: name = Path(file['filename']).name Path(name).write_bytes(base64.b64decode(file['content']))content는 base64이며, 저장된 바이트를 찾지 못하면 빈 문자열이 되므로 디코딩하기 전에 길이를 확인하십시오. 암호화된 메시지의 암호문은 이 목록에 들어 있고 다른 파일과 똑같이 내려받을 수 있지만, PGP/MIME 버전 파트와 분리된 서명은 그렇지 않습니다. 그것들은 encryption.parts에 id만 남기고 그 이상은 남기지 않습니다.
암호화된 채로 도착한 메시지
이 SDK는 암호화도 복호화도 하지 않습니다. 다른 사람이 암호화한 메시지를 열 수 없고, 암호화된 메시지를 보낼 수도 없습니다. 발송 요청에 암호화 표시가 들어 있으면 거부되는데, 키가 없는 클라이언트가 암호화를 주장할 자격은 없기 때문입니다. OpenEmail 앱에서 생성한 키는 그 키를 만든 브라우저 안에 머물며 여기에는 전혀 닿지 않고, 그 브라우저가 봉인된 메시지를 열더라도 평문은 브라우저 안에 남으며 이 호출이 읽는 저장된 메시지는 여전히 암호문입니다. threads.get이 주는 것은 식별된 봉투입니다. PGP나 S/MIME으로 감싸여 도착한 메시지에는 encryption dict가 붙으므로 빈 decodedBody만 덜렁 받는 일이 없어집니다. 그 부재를 넘겨짚었다가는 감당할 수 없는 유일한 키이며, openemail.types의 MessageEncryption이 이를 설명합니다.
import sys from openemail import is_sealed, openemail thread = openemail.threads.get('thread_…') for message in thread['messages']: if not message.get('encryption'): continue if not is_sealed(message): continue print('cannot read this one:', message['encryption']['format'], file=sys.stderr)필드의 존재 여부가 아니라 is_sealed로 분기하십시오. 다섯 가지 형식 중 pgp-signed와 smime-signed 두 가지는 분리된 서명과 함께 평문으로 도착한 본문을 뜻하므로, 존재 여부로 막으면 숨길 필요가 없던 메일까지 숨겨지고 사용자는 그것을 보지도 설명하지도 못합니다. is_sealed가 제공되는 이유가 정확히 이것입니다. 봉인된 형식의 집합은 서버가 한 번 정의하며, 유니온에서 따로 적어 낸 세 번째 사본이야말로 어긋나는 사본입니다.
부재는 평문을 뜻하지 않습니다. encryption은 탐지 기능이 출시되기 전에 저장된 모든 메시지와, 탐지기가 실행되지 않는 경로로 메일함에 들어온 모든 것에서 빠져 있습니다. 이 필드는 메일에 대한 사실이 아니라 우리 탐지 범위에 대한 사실, 즉 아무도 확인하지 않았다는 것을 기록하며, 나중에 채워 넣는 작업도 없습니다.
다른 리소스와 다른 점
ThreadResource.messages의 각 항목은MessageResource이며, 타입이 어떤 필드에도 이름을 붙이지 않은 평범한dict[str, Any]입니다.encryption조차 예외가 아닙니다. 필드에 타입을 붙이는 것은 아무도 수행하지 않는 정규화를 클라이언트가 단언하는 일이 됩니다.encryption은message.get('encryption')으로 읽고is_sealed로 분기하십시오. 이 필드로 분기하지 못하는 클라이언트는 봉인된 메시지를 빈 메시지로 읽기 때문입니다.- 충실하게 처리할 수 없는 요청은 그럴듯해 보이지만 조용히 틀린 응답이 아니라 422
capability_unsupported입니다.
매개변수: threads.list
folderstr- 어느 폴더를 조회할지입니다. 서버 기본값은 `inbox`이므로, 생략하면 전체로 넓어지는 것이 아니라 오히려 좁혀집니다. `query` 검색에도 적용되지만, 쿼리가 `in:`이나 `is:sent` 같은 폴더 `is:`로 폴더를 직접 지정하면 예외입니다.
querystr- 메일함 검색 구문입니다. 따옴표 없는 단어는 모두 나타나야 하며, 각각 느슨하게 일치합니다. 대소문자, 발음 부호, 구분 기호는 무시되고 더 긴 단어의 일부도 일치로 인정되므로 `min`과 `ben jamin` 모두 “Benjamin”을 찾아냅니다. 따옴표로 묶은 구절은 대소문자와 발음 부호를 제외하면 쓰인 그대로 일치하므로 `"ben jamin"`은 “Ben-Jamin”을 찾지 못하며, 검색할 다른 것이 남아 있으면 불용어는 버려집니다. 정확히 일치하는 것이 없으면 대신 비슷한 철자를 돌려주므로 `benjimin`은 “Benjamin”을 찾습니다. 일반 단어나 `from:`, `to:`, `cc:`, `subject:`, `body:`, `filename:`, `label:`의 값은 4~7글자이면 오타 하나(바뀐 글자, 빠진 글자, 더해진 글자, 순서가 뒤바뀐 글자), 8글자 이상이면 오타 둘까지 단어의 시작 부분과 달라도 일치하지만, 따옴표로 묶은 구절, 숫자가 든 단어, 그보다 짧은 단어, 제외한 단어는 여전히 정확히 일치해야 하며, 이어지는 페이지도 같은 방식으로 찾습니다. `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:`는 대화 전체의 모든 첨부 파일을 읽고 레이블과 폴더는 대화 전체를 읽습니다. 필터를 걸지 않은 목록이 읽는 것과 같은 색인을 좁힐 뿐입니다. 봉인된 메시지는 본문 텍스트를 저장하지 않으므로 발신자, 수신자, 제목만 일치할 수 있습니다. 일반 단어는 대화에 있는 모든 첨부 파일의 이름과도 일치하며, 어느 메시지에 붙어 있었는지는 상관없습니다.
label_idsstr | Sequence[str]- 지정한 레이블이 붙은 스레드로 목록을 제한합니다. 엔드포인트는 쉼표로 구분된 문자열을 받고, 클라이언트가 리스트나 튜플을 하나의 문자열로 이어 줍니다. 몇 개까지 지정할 수 있는지에 대한 제한은 없습니다.
limitint- 반환할 스레드 수이며 1에서 100까지입니다. 생략하면 핸들러가 25를 사용합니다. 기본값이 스키마가 아니라 핸들러에 있으므로, 값을 생략하는 것과 25를 명시하는 것이 똑같이 동작합니다.
cursorstr- 이전 페이지의 `nextCursor`를 그대로 다시 보냅니다. 다른 모든 목록이 쓰는 이름으로 감싼 API의 `pageToken`이며 불투명하므로, 직접 만들거나 수정하지 마십시오.
응답: Page[ThreadSummaryResource]
itemslist[ThreadSummaryResource]- 이 페이지에 담긴 스레드마다 하나씩이며, API의 `data` 봉투에서 꺼낸 것입니다. 각 항목에는 객체 표시자와 id밖에 없습니다. 목록에는 제목, 스니펫, 참여자, 레이블이 전혀 담기지 않으므로, 그 이상이 필요하면 원하는 스레드에 대해 `threads.get`을 호출해야 합니다.
items[].idstr- 스레드의 id이며, `threads.get`, `threads.update` 등에 그대로 넘기면 됩니다. 필터를 건 목록에서 나온 행이든 `query` 검색에서 나온 행이든 같은 id입니다.
hasMorebool- 다음 페이지가 있는지 여부이며, API가 명시하지 않는 경우 `nextCursor`에서 파생합니다.
nextCursorstr | None- 다음 페이지를 위해 `cursor`로 돌려보낼 API의 `nextPageToken`이며, 다음 페이지가 없으면 `None`입니다. 빈 토큰은 `None`으로 정규화되므로 falsy 검사와 `None` 검사의 결과가 일치합니다.