スレッド
`threads.list`、`list_all`、`iterate`、`get`、`update`、`trash`、`snooze`、`unsnooze`、`list_attachments`。
読む
page = client.threads.list( folder: "inbox", query: "from:ada", label_ids: ["INBOX", "IMPORTANT"], limit: 25) if page.next_cursor next_page = client.threads.list(folder: "inbox", cursor: page.next_cursor) puts next_page.items.sizeend thread = client.threads.get("CAHk7pQ2x9LmZ4-mail.example.com")puts thread[:messageCount], thread[:hasUnread], thread[:totalReplies]API はスレッドを pageToken でページ分割します。クライアントは他のすべての一覧と同じように、それを next_cursor として渡し、cursor: として受け取ります。list_all と iterate はそれを自動的にたどります。これは不透明な値です:受け取ったものをそのまま渡し返し、自分で組み立てないでください。
一覧のフィルターは snake_case の Ruby のキーワード引数(label_ids:、date_from:)ですが、リクエストボディのフィールドは API の camelCase の名前のままです(update の addLabelIds:)。スレッドは Symbol キーの Hash として返るため、thread[:messageCount] で件数を読めます。
last_week = client.threads.list_all( sort: "oldest", date_from: Time.now - (7 * 86_400), date_to: Time.now, from_contacts: true)puts last_week.size client.threads.iterate(sort: "sender") do |thread| puts thread[:id]endsort:、date_from:、date_to:、from_contacts: はスレッド一覧独自の指定です。sort: は newest、oldest、sender、subject のいずれかで、OpenEmail::THREAD_SORTS がそれらを定義しています。日付には Time、DateTime、または時刻とオフセットを含む ISO 8601 の文字列を渡し、両端を含みます。Ruby の Date は日付だけの値として送られ、これらのフィールドはそれを 422 で拒否します。from_contacts: true は、最新のメッセージが保存済みの連絡先から届いたメールだけを残します。どの並び順でも、スレッドを飛ばしたり重複させたりせずに最後までページ分割されます。
list_all は最後のページを取得した時点で 1 つの Array を返します。iterate は各スレッドをブロックに yield し、ループが必要とするときにだけ次のページを取得します。ブロックがなければ Enumerator を返すため、first(10) や lazy は必要なものがそろった時点で止まります。
整理
thread_id = "CAHk7pQ2x9LmZ4-mail.example.com" client.threads.update(thread_id, read: true, addLabelIds: ["USER_DONE"], removeLabelIds: ["INBOX"]) client.threads.trash(thread_id)client.threads.snooze(thread_id, Time.now + 86_400)client.threads.unsnooze(thread_id)既読状態はここではどのバックエンドでもラベルなので、ラベルのリストと一緒に扱われます。両方を指定したときの順序は決まっています:削除が追加より先に適用されるため、両方のリストにある id は最終的にスレッドに付きます。3 つのフィールドのうち少なくとも 1 つが必要です。
addLabelIds は labels.list から得た id と、ARCHIVE や STARRED のようなシステム id を受け取ります。どのラベルも指さない id は、作成されるのではなく 422 label_not_found で拒否されるため、先に labels.create でラベルを作ってください。client.threads.list(folder: "USER_DONE") は、どのフォルダーにあるかにかかわらず、そのラベルが付いたすべてのスレッドを一覧にします。
メッセージの添付ファイル
files = client.threads.list_attachments("CAHk7pQ2x9LmZ4-mail.example.com", "message_4c1b257a") files.each do |file| puts "#{file[:filename]} #{file[:contentType]} #{file[:size]}" File.binwrite(file[:filename], file[:content].unpack1("m")) unless file[:content].to_s.empty?endlist_attachments は Hash の Array を返します。content は base64 で、unpack1("m") でバイナリの String に変換できます。保存されたバイト列が見つからなかった場合は空の文字列になるため、デコードする前に長さを確認してください。暗号化されたメッセージの暗号文はこの一覧に含まれ、他のファイルと同じようにダウンロードできます。PGP/MIME のバージョン部分と分離署名は含まれません。それらは encryption.parts に id が残るだけです。
暗号化されて届いたメッセージ
この gem は暗号化も復号もしません。他の誰かが暗号化したメッセージを開くことも、暗号化したメッセージを送ることもできません。送信リクエストに暗号化のマーカーが付いていると拒否されます。鍵を持たないクライアントが暗号化を主張する理由はないからです。OpenEmail アプリで生成された鍵は、それを作ったブラウザーの中だけにあり、ここには届きません。そのブラウザーが封緘されたメッセージを開いても、平文はブラウザーの中にとどまり、この呼び出しが読む保存済みのメッセージは暗号文のままです。threads.get が返すのは、識別済みのエンベロープです。PGP または S/MIME で包まれて届いたメッセージには encryption の Hash が付くため、渡されるものが空の decodedBody だけではなくなります。encryption は API が保証するメッセージの唯一のフィールドです。これがないことを推測で済ませては立ち行かない、唯一のフィールドだからです。
thread = client.threads.get("CAHk7pQ2x9LmZ4-mail.example.com") thread[:messages].each do |message| next unless message[:encryption] next unless OpenEmail.sealed?(message) warn "cannot read this one: #{message[:encryption][:format]}"end分岐には OpenEmail.sealed? を使い、フィールドの有無で分岐しないでください。5 つの形式のうち pgp-signed と smime-signed の 2 つは、分離署名とともに平文で届いたボディを表すため、有無で判定すると隠す必要のなかったメールまで隠してしまい、ユーザーはそれを見ることも説明することもできません。OpenEmail.sealed? はまさにそのためにあります。サーバーが封緘された形式の集合を一度だけ定め、gem の写しは同じ情報源から生成されます。手で書き写した 3 つ目の写しこそが、食い違っていく写しです。OpenEmail::MESSAGE_ENCRYPTION_FORMATS は 5 つの形式すべてを定義しています。
値がないことは平文であることを意味しない。encryption は、検出機能が導入される前に保存されたすべてのメッセージと、検出器が動作しない経路でメールボックスに届いたものには存在しない。これは「誰も確認しなかった」という記録であり、メールについての事実ではなく、こちらの検出範囲についての事実である。後から埋め直されることもない。
他と異なる点
- スレッドの
messagesの各エントリは、メールボックスが保存した Hash そのもので、決まったフィールドの一覧はありません。それ以上を約束すれば、誰も行っていない正規化をクライアントが主張することになります。それでもencryptionだけは API が保証するフィールドです。これで分岐できないクライアントは、封緘されたメッセージを空のメッセージとして読んでしまうからです。 - 正確に応えられないリクエストは、正しく見えて実は誤っているレスポンスではなく、
OpenEmail::ValidationErrorとして送出される 422capability_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 文字なら 1 つ、8 文字以上なら 2 つまでのタイプミス(文字の置き換え、欠落、余分、入れ替え)があっても単語の先頭に一致します。引用符で囲んだフレーズ、数字を含む語、それより短い語、除外する語は引き続き完全一致のみで、続くページも同じ方法で一致します。`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_idsString or Array<String>- 一覧を、これらのラベルが付いたスレッドに限定します。エンドポイントはカンマ区切りの文字列を受け取り、クライアントは Array や Set を自動的に 1 つの文字列に結合します。指定できる数に上限はありません。
limitInteger- 返すスレッドの数で、1〜100。省略するとハンドラーは 25 を使います。既定値はスキーマではなくハンドラーにあるため、値がない場合と明示的な 25 は同じように動作します。
cursorString- 前のページの `next_cursor` を、そのまま渡し返します。他のすべての一覧と同じ名前で扱う API の `pageToken` で、不透明な値なので、決して自分で組み立てたり編集したりしないでください。
レスポンス:OpenEmail::Page
itemsArray<Hash>- このページのスレッドごとに 1 つの Hash で、API の `data` エンベロープから取り出したものです。それぞれ `object` マーカーと `id` だけを持ちます。一覧には件名、スニペット、参加者、ラベルが含まれないため、それ以上が必要なら、目的のスレッドに対して `threads.get` を呼び出してください。
items[].idString- スレッドの id で、`item[:id]` として読み、そのまま `threads.get`、`threads.update` などに渡します。行が絞り込んだ一覧から来たものでも `query:` の検索から来たものでも、同じ id です。
has_more?Boolean- 次のページがあるかどうか。API が示している場合はその値を使い、示していない場合は `next_cursor` から導きます。
next_cursorString or nil- API の `nextPageToken` で、次のページのために `cursor:` として送り返します。次のページがない場合は nil です。空のトークンは nil に正規化されるため、`if page.next_cursor` と nil のチェックの結果は一致します。