ドキュメント本文へスキップ
SDK

スレッド

`threads.list`、`listAll`、`iterate`、`get`、`update`、`trash`、`snooze`、`unsnooze`、`listAttachments`。

読み取り

read-threads.ts
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 として受け取る。listAlliterate は自動でたどる。この値は不透明であり、受け取ったものをそのまま返すこと。決して自分で組み立ててはならない。

整理

organise-threads.ts
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_…')

ここで扱うどのバックエンドでも既読状態はラベルそのものであるため、ラベルのリストと一緒に扱われ、両方を設定した場合の適用順序は決定的である。3 つのフィールドのうち少なくとも 1 つは指定しなければならない。

メッセージの添付ファイル

attachments.ts
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 だけではなくなる。encryptionMessageResource 上で唯一きちんとした型を持つフィールドである。これが欠けていることを推測で済ませては立ち行かない、唯一のフィールドだからである。

encrypted-mail.ts
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 を使うこと。5 つの形式のうち pgp-signedsmime-signed の 2 つは、分離署名とともに平文で届いた本文を表す。そのため有無で判定すると、隠す必要のないメールまで隠してしまい、ユーザーはそれを見ることも説明することもできなくなる。isSealed が提供されているのはまさにそのためである。封緘された形式の集合はサーバーが一度だけ宣言するものであり、ユニオンから書き起こした 3 つ目の写しこそが食い違っていく写しである。

値がないことは平文であることを意味しない。encryption は、検出機能が導入される前に保存されたすべてのメッセージと、検出器が動作しない経路でメールボックスに届いたものには存在しない。これは「誰も確認しなかった」という記録であり、メールについての事実ではなく、こちらの検出範囲についての事実である。後から埋め直されることもない。

他と異なる点

  • ThreadResource.messages の各要素は MessageResource であり、名前の付いたフィールドをちょうど 1 つだけ持つ 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[]
これらのラベルが付いたスレッドだけに一覧を絞り込む。エンドポイントはカンマ区切りの文字列を受け取り、クライアントが配列を 1 つの文字列に連結する。指定できる個数に上限はない。
limitnumber
返すスレッドの件数。1 〜 100。省略した場合、ハンドラーは 25 を使う。既定値はスキーマではなくハンドラー側にあるため、値を指定しない場合と明示的に 25 を指定した場合の動作は同じである。
cursorstring
前のページの `nextCursor` をそのまま渡す。これは API の `pageToken` を、他のすべての一覧が使う名前で表したものである。不透明な値なので、自分で組み立てたり編集したりしてはならない。

レスポンス: Page<ThreadSummaryResource>

itemsThreadSummaryResource[]
このページに含まれるスレッドごとに 1 エントリ。API の `data` エンベロープから取り出されている。各エントリはオブジェクトの種別マーカーと id だけである。一覧には件名、抜粋、参加者、ラベルは含まれないため、それ以上の情報が必要であれば、対象のスレッドに対して `threads.get` を呼ぶことになる。
items[].idstring
スレッドの id。`threads.get` や `threads.update` などにそのまま渡す。その行がフィルター付きの一覧から来たものであっても `query` による検索から来たものであっても、id は同じである。
hasMoreboolean
さらにページがあるかどうか。API が明示しない場合は `nextCursor` から導出される。
nextCursorstring | null
API の `nextPageToken`。次のページを取得する際に `cursor` として送り返す。さらにページがない場合は null。空のトークンは null に正規化されるため、falsy かどうかの検査と null かどうかの検査は一致する。