문서로 건너뛰기
PHP

스레드

`threads->list`, `listAll`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze`, `listAttachments`.

읽기

read_threads.php
$page = $client->threads->list(    folder: 'inbox',    query: 'from:ada',    labelIds: ['INBOX', 'IMPORTANT'],    limit: 25,); if ($page->nextCursor !== null) {    $nextPage = $client->threads->list(folder: 'inbox', cursor: $page->nextCursor);    echo count($nextPage), PHP_EOL;} $thread = $client->threads->get('CAHk7pQ2x9LmZ4-mail.example.com');echo $thread['messageCount'], ' ', $thread['hasUnread'] ? 'unread' : 'read', ' ', $thread['totalReplies'], PHP_EOL;

API는 스레드를 pageToken으로 페이지 처리합니다. 클라이언트는 다른 모든 목록과 마찬가지로 이를 nextCursor로 건네주고 cursor:로 돌려받으며, listAll과 iterate가 대신 따라갑니다. 이 값은 불투명합니다: 받은 것을 그대로 돌려주고 직접 만들지 마세요.

목록 필터는 명명된 인자(labelIds:, dateFrom:)이고, 요청 본문의 필드는 API의 이름을 그대로 쓴 배열 키입니다(update의 addLabelIds). 스레드는 camelCase 키를 가진 배열로 돌아오므로, $thread['messageCount']로 개수를 읽습니다.

sort_threads.php
use OpenEmail\Constants\ThreadSorts; $lastWeek = $client->threads->listAll(    sort: ThreadSorts::OLDEST,    dateFrom: new \DateTimeImmutable('-7 days'),    dateTo: new \DateTimeImmutable(),    fromContacts: true,);echo count($lastWeek), PHP_EOL; foreach ($client->threads->iterate(sort: ThreadSorts::SENDER) as $thread) {    echo $thread['id'], PHP_EOL;}

sort:, dateFrom:, dateTo:, fromContacts:는 스레드 목록 자체의 조작입니다. sort:는 newest, oldest, sender, subject 중 하나이며, OpenEmail\Constants\ThreadSorts가 이들을 정의합니다. 날짜는 UTC 시각으로 전송되는 DateTimeInterface, 또는 시각과 오프셋이 있는 ISO 8601 문자열을 받으며, 양 끝을 모두 포함합니다. 시각이 없는 날짜 문자열은 422로 거부됩니다. fromContacts: true는 최신 메시지가 저장된 연락처에서 온 메일만 남깁니다. 어느 정렬이든 스레드를 건너뛰거나 반복하지 않고 끝까지 페이지를 넘깁니다.

listAll은 마지막 페이지가 들어오면 하나의 배열을 반환합니다. iterate는 각 스레드를 yield하고 루프가 필요로 할 때만 다음 페이지를 가져오는 Generator를 반환하므로, 필요한 것을 얻는 즉시 break로 요청을 멈출 수 있습니다.

정리

organise_threads.php
$threadId = 'CAHk7pQ2x9LmZ4-mail.example.com'; $client->threads->update($threadId, ['read' => true, 'addLabelIds' => ['USER_DONE'], 'removeLabelIds' => ['INBOX']]); $client->threads->trash($threadId);$client->threads->snooze($threadId, new \DateTimeImmutable('+1 day'));$client->threads->unsnooze($threadId);

여기서는 어떤 백엔드에서든 읽음 상태가 곧 라벨이므로 라벨 목록과 함께 전달되며, 둘 다 설정할 때의 순서는 정해져 있습니다: 제거가 추가보다 먼저 적용되므로, 두 목록 모두에 있는 id는 결국 스레드에 붙습니다. 세 필드 중 최소 하나는 반드시 있어야 합니다.

addLabelIds는 labels->list에서 얻은 id와 ARCHIVE, STARRED 같은 시스템 id를 받습니다. 어떤 라벨도 가리키지 않는 id는 만들어지지 않고 422 label_not_found로 거부되므로, 먼저 labels->create로 라벨을 만드세요. $client->threads->list(folder: 'USER_DONE')는 어느 폴더에 있든 그 라벨이 붙은 모든 스레드를 나열합니다.

메시지의 첨부 파일

attachments.php
$files = $client->threads->listAttachments('CAHk7pQ2x9LmZ4-mail.example.com', 'message_4c1b257a'); foreach ($files as $file) {    echo $file['filename'], ' ', $file['contentType'], ' ', $file['size'], PHP_EOL;     $bytes = base64_decode($file['content'], true);     if ($file['content'] !== '' && $bytes !== false) {        file_put_contents(basename($file['filename']), $bytes);    }}

listAttachments는 배열의 리스트를 반환합니다. content는 base64이며 base64_decode()로 다시 바이트로 바꿀 수 있고, 저장된 바이트를 찾지 못하면 빈 문자열이 되므로 디코딩하기 전에 확인하세요. 암호화된 메시지의 암호문은 이 목록에 들어 있고 다른 파일과 똑같이 내려받을 수 있습니다. PGP/MIME 버전 파트와 분리된 서명은 그렇지 않습니다. 그것들은 encryption.parts에 id만 남기고 그 이상은 남기지 않습니다.

암호화된 채로 도착한 메시지

이 패키지는 암호화도 복호화도 하지 않습니다. 다른 사람이 암호화한 메시지를 열 수 없고, 암호화된 메시지를 보낼 수도 없습니다. 발송 요청에 암호화 표시가 들어 있으면 거부되는데, 키가 없는 클라이언트가 암호화를 주장할 자격은 없기 때문입니다. OpenEmail 앱에서 생성한 키는 그 키를 만든 브라우저 안에 머물며 여기에는 전혀 닿지 않습니다. 그 브라우저가 봉인된 메시지를 열더라도 평문은 브라우저 안에 남으며, 이 호출이 읽는 저장된 메시지는 여전히 암호문입니다. threads->get이 주는 것은 식별된 봉투입니다. PGP나 S/MIME으로 감싸여 도착한 메시지에는 encryption 배열이 붙으므로, 빈 decodedBody만 덜렁 받는 일이 없어집니다. encryption은 API가 보장하는 메시지의 유일한 필드입니다. 그 부재를 넘겨짚었다가는 감당할 수 없는 유일한 필드이기 때문입니다.

encrypted_mail.php
use OpenEmail\OpenEmail; $thread = $client->threads->get('CAHk7pQ2x9LmZ4-mail.example.com'); foreach ($thread['messages'] as $message) {    if (!isset($message['encryption']) || !OpenEmail::isSealed($message)) {        continue;    }     error_log('cannot read this one: ' . $message['encryption']['format']);}

필드의 존재 여부가 아니라 OpenEmail::isSealed()로 분기하세요. 다섯 가지 형식 중 pgp-signed와 smime-signed 두 가지는 분리된 서명과 함께 평문으로 도착한 본문을 뜻하므로, 존재 여부로 막으면 숨길 필요가 없던 메일까지 숨겨지고 사용자는 그것을 보지도 설명하지도 못합니다. OpenEmail::isSealed()가 있는 이유가 정확히 이것입니다. 봉인된 형식의 집합은 서버가 한 번 정의하고, 패키지의 사본은 같은 원천에서 생성되며, 손으로 따로 적어 낸 세 번째 사본이야말로 어긋나는 사본입니다. OpenEmail\Constants\MessageEncryptionFormats가 다섯 형식을 모두 정의합니다.

부재는 평문을 뜻하지 않습니다. encryption은 탐지 기능이 출시되기 전에 저장된 모든 메시지와, 탐지기가 실행되지 않는 경로로 메일함에 들어온 모든 것에서 빠져 있습니다. 이 필드는 메일에 대한 사실이 아니라 우리 탐지 범위에 대한 사실, 즉 아무도 확인하지 않았다는 것을 기록하며, 나중에 채워 넣는 작업도 없습니다.

다른 리소스와 다른 점

  • 스레드의 messages의 각 항목은 메일함이 저장한 배열이며, 정해진 필드 목록이 없으므로 encryption 이외의 키는 ?? null로 읽으세요. 그 이상을 약속하는 것은 아무도 수행하지 않는 정규화를 클라이언트가 단언하는 일이 됩니다. 그럼에도 encryption만은 API가 보장하는 필드인데, 이 필드로 분기하지 못하는 클라이언트는 봉인된 메시지를 빈 메시지로 읽기 때문입니다.
  • 충실하게 처리할 수 없는 요청은 그럴듯해 보이지만 조용히 틀린 응답이 아니라, ValidationException으로 던져지는 422 capability_unsupported입니다.

매개변수: threads->list

folderstring
어느 폴더를 조회할지입니다. 서버 기본값은 `inbox`이므로, 생략하면 전체로 넓어지는 것이 아니라 오히려 좁혀집니다. `query:` 검색에도 적용되지만, 쿼리가 `in:`이나 `is:sent` 같은 폴더 `is:`로 폴더를 직접 지정하면 예외입니다.
querystring
메일함 검색 구문입니다. 따옴표 없는 단어는 모두 나타나야 하며, 각각 느슨하게 일치합니다: 대소문자, 발음 부호, 구분 기호는 무시되고 더 긴 단어의 일부도 일치로 인정되므로 `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:`는 대화 전체의 모든 첨부 파일을 읽고, 라벨과 폴더는 대화 전체를 읽습니다. 필터를 걸지 않은 목록이 읽는 것과 같은 색인을 좁힐 뿐입니다. 봉인된 메시지는 본문 텍스트를 저장하지 않으므로 발신자, 수신자, 제목만 일치할 수 있습니다. 일반 단어는 대화에 있는 모든 첨부 파일의 이름과도 일치하며, 어느 메시지에 붙어 있었는지는 상관없습니다.
labelIdsstring or array
이 라벨들이 붙은 스레드로 목록을 제한합니다. 엔드포인트는 쉼표로 구분된 문자열을 받고, 클라이언트가 배열을 하나의 문자열로 이어 줍니다. 몇 개까지 지정할 수 있는지에 대한 제한은 없습니다.
limitint
반환할 스레드 수이며 1에서 100까지입니다. 생략하면 핸들러가 25를 사용합니다. 기본값이 스키마가 아니라 핸들러에 있으므로, 값을 생략하는 것과 25를 명시하는 것이 똑같이 동작합니다.
cursorstring
이전 페이지의 `nextCursor`를 그대로 다시 보냅니다. 다른 모든 목록이 쓰는 이름으로 감싼 API의 `pageToken`이며 불투명하므로, 직접 만들거나 수정하지 마세요.

응답: OpenEmail\Result\Page

itemsarray
이 페이지에 담긴 스레드마다 하나씩인 배열이며, API의 `data` 봉투에서 꺼낸 것입니다. 각 항목에는 `object` 표시자와 `id`밖에 없습니다. 목록에는 제목, 스니펫, 참여자, 라벨이 전혀 담기지 않으므로, 그 이상이 필요하면 원하는 스레드에 대해 `threads->get`을 호출해야 합니다.
items[].idstring
스레드의 id로, `$item['id']`로 읽으며 `threads->get`, `threads->update` 등에 그대로 넘기면 됩니다. 필터를 건 목록에서 나온 행이든 `query:` 검색에서 나온 행이든 같은 id입니다.
hasMorebool
다음 페이지가 있는지 여부이며, API가 명시하면 그 값을 쓰고 명시하지 않으면 `nextCursor`에서 파생합니다.
nextCursorstring or null
다음 페이지를 위해 `cursor:`로 돌려보낼 API의 `nextPageToken`이며, 다음 페이지가 없으면 null입니다. 빈 토큰은 null로 정규화되므로 null 검사만 하면 충분합니다.